Explorer drag and drop. Local settings.

This commit is contained in:
Benoit Schweblin 2017-08-03 18:08:12 +01:00
parent 0c27a8337a
commit c3d8933a9e
26 changed files with 638 additions and 210 deletions

54
src/assets/logo.svg Normal file
View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="744" height="168" viewBox="0, 0, 744, 168">
<g id="Layer_3">
<path d="M138,-0 C143.016,0.004 147.86,1.294 152.3,3.621 C153.588,4.296 154.711,5.249 155.916,6.063 L84,60 L12.084,6.063 C17.954,1.677 22.777,0.365 30,-0 z" fill="#FFE600"/>
<path d="M12.084,6.063 L84,60 L6.614,156.733 C2.027,150.844 0.372,145.362 -0,138 L-0,30 C0.086,23.039 2.401,16.314 6.851,10.917 C8.07,9.438 9.562,8.206 10.917,6.851 z" fill="#BBD500"/>
<path d="M84,60 L161.386,156.732 L161.149,157.083 C154.615,164.37 147.725,167.509 138,168 L30,168 C23.039,167.914 16.314,165.599 10.917,161.15 C9.438,159.93 8.206,158.438 6.851,157.083 L6.614,156.733 z" fill="#FF8A00"/>
<path d="M155.916,6.063 C161.042,9.571 164.828,15.169 166.651,21.079 C167.238,22.982 167.548,24.964 167.845,26.933 C167.998,27.945 167.948,28.978 168,30 L168,138 C167.97,142.998 166.748,147.886 164.379,152.3 C163.536,153.871 162.384,155.255 161.386,156.733 L84,60 z" fill="#75B7FD"/>
</g>
<g id="Layer_1">
<path d="M42.75,12 L125.25,12 C135.605,12 144,20.395 144,30.75 L144,113.25 C144,123.605 135.605,132 125.25,132 L42.75,132 C32.395,132 24,123.605 24,113.25 L24,30.75 C24,20.395 32.395,12 42.75,12 z" fill="#FFFFFF"/>
<g>
<path d="M39.511,41.766 L50.811,41.766 L53.893,25.332 L60.827,25.332 L57.745,41.766 L70.77,41.766 L73.778,25.332 L80.712,25.332 L77.704,41.766 L83.941,41.766 L83.941,49.875 L76.383,49.875 L74.072,62.876 L83.941,62.876 L83.941,70.986 L72.751,70.986 L69.742,87.29 L62.772,87.29 L65.817,70.986 L52.646,70.986 L49.784,87.29 L42.777,87.29 L45.748,70.986 L39.511,70.986 L39.511,62.876 L47.179,62.876 L49.491,49.875 L39.511,49.875 z M56.425,49.875 L54.113,62.833 L67.174,62.833 L69.449,49.875 z" fill="#737373"/>
<path d="M39.511,41.766 L50.811,41.766 L53.893,25.332 L60.827,25.332 L57.745,41.766 L70.77,41.766 L73.778,25.332 L80.712,25.332 L77.704,41.766 L83.941,41.766 L83.941,49.875 L76.383,49.875 L74.072,62.876 L83.941,62.876 L83.941,70.986 L72.751,70.986 L69.742,87.29 L62.772,87.29 L65.817,70.986 L52.646,70.986 L49.784,87.29 L42.777,87.29 L45.748,70.986 L39.511,70.986 L39.511,62.876 L47.179,62.876 L49.491,49.875 L39.511,49.875 z M56.425,49.875 L54.113,62.833 L67.174,62.833 L69.449,49.875 z" fill-opacity="0" stroke="#737373" stroke-width="3"/>
<image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAABBCAYAAACXfCfjAAAGCklEQVRoBdWZ2ZLbOAxFk9lqtv//0llqlk6GR/ZRrmCSotx+yKBKxnZxAZByp7vy8cPz8nMr/aU9n+9PZfrYAjx/tOf3muz4v7bYT+054/utYf60/juNJ/S3reatPTZEKw7/TQv8a/BEr/L9kzzvWYAhWeBTe1xCbnIMT/zQUEBHr/IdDuRVC7gEc3n6LkXuTFyWA8lbpU4+7AeuZxfguiHzBuoCDITka3WL9D99fWZ8LPLyBRgQ0roAY3YbkuiINyBX5ZOL+EGevYHa0NflQN4cTnRF4HNol7CO4b3JBz6vWvCqdgGJRws8nNigQfIlJ3B97Ae+9y5QG+BzYojXfvPmn9bksFaQ83nIP7PA7LSy6UMzk0XzBQab+LSFP5w+iWcWoOGK9Ibo1eUCdRFPHt3le2aBegNJnA27J9bZQL5O6hDq8r13gezA8IhLdBveIIdPZ8iDkEcNZ5fP4gPjiZMnVptmabdhAu42w8mjrrCXLQARQqNeM/Ni0DPxAJMveeUbHoYEsyaZ8/RtqE4M9rBhAfb4CmRzc6lDnn+J+T1c6QEd0hzDaVunzvgPLciAnKIPuOQjPuMDr8iXnH+xwI/tSVIKchBzNPKXLWNgq5CjCQ8/IrNhc/deK3z2SS45ib2xAEQ+FCBqbYkSZ2wriA/i4BTs3g0QlyO1derKR5wY8pkFPFWH2xJ83GUHNz+bmleLwweHEMvhjamv8llnr20BSHIJk4ARfbRPxrEVsfguwQKIWoxc6d+QXz7NEZHPGHzbAgRI+uALauYuGUt7B9wNc+itSQUUHK41Pag5Ne8+s24HwitEgicXaO5BLD4EJ454tadviXH9Mw3ew3B4YtsNZPEWzMCL7KsD99rK4SIbhuv4XwuvUMp7r3rGRc5TTNyqzWzOp/7AAia8jXyNyNlUPWsosbpi38tHPXOit4d/1TD4YuTgzX0Qix4SLWAOjahvXv/Tml7WnDxVU8O8byZ6JBnr3Y55OcBAymGcycv4JDpryJBg0Q6cNaN4YtKGa4Xv9LVdXcBXrQ7v4Gi4ThvetxgNLx/5JT5AK2LDEdb81QVGfC4yyu/xlQUcbi8qxnKze91L+VYW8PUpcz+4q6c/4/Mw0Et8KwuAGZFlw5WfPmw94yOvLPGtLgBpXYLhU5YatgJ7wpecyYe9xCdZDlJtyGyWDStuqWErcrgZF7klPn6V4D/rkkwbrZ1kxlr6IN83j+E4FHQVOORUgxnxMZtc8FVO/L9zAUnVNPQvNWPZDBsSczakqY2buYnDJx+J5Lshb5/EORB4/NK7ROr9T0obqB0K36fXjJhLgEOIYRNXjKHrEonBBiNen0WQHJ7YvoCkNJZgpCFKsZl4m1SM+dojcdjyacOXB5K3uy8AwCVsJIE+ukqNwTFbgPpVPmZCks9lvJHDAhTw2IBiB1QTq2IOTQP9isM3pz7DJB824vCb5gvCl8XTdwGAsybkezKrmeV6XMSyRptFfD7xk6OKwBpf9d9bX/tUPvw95nVQdEhUlq/VzxvwWlzma5rZ97/qwx/1ucDqEhKCTzuX97rVmat2cqQtjhhvDXp7+BITyC9vczcRpF915rERY/rG1BknlmItscQZrzFw/PAZSv4TXkGQsXh+hyqm+it8YC7JaADibl4JjYup+Z4vNk9RnHy9nJihfmYByGw6JC6J2QLyjWYpVEd3VOR1S26Vg6OpXfliUiuf9cQUY+hVPmuH73A23MFh2JQv/4p4AxULD4K+ciBbER+9G3C4HVSMs3yB78PVuL5L4L/kBjj9JErbpleawXcmLDHqM63t3YANIayknj76yusjl9qhkq/2EjPVvQVG7yLNFOzVhvVGkyPt1QOxZtOzBeppWegiqw3t8So+5xgu4IC9EzaHXl0AbI+LAZ7ho24XT8eA150N0wY3G0gedfLJoxZzhc+aXdcF8GmQzw5uBs14Vk9fPjjkxFau8lm36/x7gKA/gbAlx0b0rywgH8Nbv5GFf4XP2l33FrAZIMgVTtMhVm/AV0geNZzP8DnLrusCkDIcjdKmwOHRqwskR9qVb/p7PeCR/AeSOSR/DNOWqgAAAABJRU5ErkJggg==" preserveAspectRatio="none" x="38.011" y="23.832" width="48" height="65"/>
</g>
<g>
<path d="M204.762,116.632 L204.762,101.213 Q218.931,107.284 229.009,107.284 Q236.064,107.284 240.125,104.066 Q244.186,100.849 244.186,95.325 Q244.186,90.954 241.637,87.949 Q239.087,84.944 231.973,80.817 L226.104,77.478 Q214.306,70.679 209.445,64.457 Q204.584,58.234 204.584,49.918 Q204.584,38.809 212.557,32.071 Q220.531,25.332 233.633,25.332 Q244.127,25.332 257.288,28.428 L257.288,42.815 Q243.83,37.655 236.005,37.655 Q229.957,37.655 226.311,40.418 Q222.665,43.18 222.665,47.672 Q222.665,51.375 225.215,54.167 Q227.764,56.96 234.819,61.027 L241.103,64.608 Q253.79,71.893 258.592,78.024 Q263.394,84.155 263.394,92.957 Q263.394,105.463 254.323,112.565 Q245.253,119.668 229.305,119.668 Q218.753,119.668 204.762,116.632 z" fill="#555555"/>
<path d="M204.762,116.632 L204.762,101.213 Q218.931,107.284 229.009,107.284 Q236.064,107.284 240.125,104.066 Q244.186,100.849 244.186,95.325 Q244.186,90.954 241.637,87.949 Q239.087,84.944 231.973,80.817 L226.104,77.478 Q214.306,70.679 209.445,64.457 Q204.584,58.234 204.584,49.918 Q204.584,38.809 212.557,32.071 Q220.531,25.332 233.633,25.332 Q244.127,25.332 257.288,28.428 L257.288,42.815 Q243.83,37.655 236.005,37.655 Q229.957,37.655 226.311,40.418 Q222.665,43.18 222.665,47.672 Q222.665,51.375 225.215,54.167 Q227.764,56.96 234.819,61.027 L241.103,64.608 Q253.79,71.893 258.592,78.024 Q263.394,84.155 263.394,92.957 Q263.394,105.463 254.323,112.565 Q245.253,119.668 229.305,119.668 Q218.753,119.668 204.762,116.632 z" fill-opacity="0" stroke="#555555" stroke-width="3"/>
</g>
<g>
<path d="M309.298,117.057 Q303.014,118.939 299.397,118.939 Q276.573,118.939 276.573,97.085 L276.573,61.998 L269.281,61.998 L269.281,50.768 L276.573,50.768 L276.573,39.537 L294.121,37.473 L294.121,50.768 L308.053,50.768 L308.053,61.998 L294.121,61.998 L294.121,94.718 Q294.121,106.98 303.903,106.98 Q306.156,106.98 309.298,106.13 z" fill="#555555"/>
<path d="M309.298,117.057 Q303.014,118.939 299.397,118.939 Q276.573,118.939 276.573,97.085 L276.573,61.998 L269.281,61.998 L269.281,50.768 L276.573,50.768 L276.573,39.537 L294.121,37.473 L294.121,50.768 L308.053,50.768 L308.053,61.998 L294.121,61.998 L294.121,94.718 Q294.121,106.98 303.903,106.98 Q306.156,106.98 309.298,106.13 z" fill-opacity="0" stroke="#555555" stroke-width="3"/>
</g>
<g>
<path d="M356.521,110.258 Q347.747,118.939 337.728,118.939 Q329.191,118.939 323.856,113.597 Q318.52,108.255 318.52,99.756 Q318.52,88.708 327.146,82.729 Q335.772,76.749 351.838,76.749 L356.521,76.749 L356.521,70.679 Q356.521,60.298 344.961,60.298 Q334.705,60.298 324.211,66.247 L324.211,53.864 Q336.128,49.25 347.807,49.25 Q373.358,49.25 373.358,70.072 L373.358,99.574 Q373.358,107.405 378.279,107.405 Q379.168,107.405 380.591,107.162 L381.006,117.239 Q375.433,118.939 371.165,118.939 Q360.375,118.939 357.292,110.258 z M356.521,100.606 L356.521,87.069 L352.371,87.069 Q335.357,87.069 335.357,97.996 Q335.357,101.699 337.817,104.218 Q340.277,106.737 343.894,106.737 Q350.059,106.737 356.521,100.606 z" fill="#555555"/>
<path d="M356.521,110.258 Q347.747,118.939 337.728,118.939 Q329.191,118.939 323.856,113.597 Q318.52,108.255 318.52,99.756 Q318.52,88.708 327.146,82.729 Q335.772,76.749 351.838,76.749 L356.521,76.749 L356.521,70.679 Q356.521,60.298 344.961,60.298 Q334.705,60.298 324.211,66.247 L324.211,53.864 Q336.128,49.25 347.807,49.25 Q373.358,49.25 373.358,70.072 L373.358,99.574 Q373.358,107.405 378.279,107.405 Q379.168,107.405 380.591,107.162 L381.006,117.239 Q375.433,118.939 371.165,118.939 Q360.375,118.939 357.292,110.258 z M356.521,100.606 L356.521,87.069 L352.371,87.069 Q335.357,87.069 335.357,97.996 Q335.357,101.699 337.817,104.218 Q340.277,106.737 343.894,106.737 Q350.059,106.737 356.521,100.606 z" fill-opacity="0" stroke="#555555" stroke-width="3"/>
</g>
<g>
<path d="M439.656,115.965 Q428.747,118.939 419.439,118.939 Q403.848,118.939 394.807,109.56 Q385.766,100.181 385.766,84.034 Q385.766,67.704 395.074,58.477 Q404.381,49.25 420.803,49.25 Q428.747,49.25 439.122,51.86 L439.122,64.851 Q428.332,61.27 421.87,61.27 Q413.985,61.27 409.183,67.461 Q404.381,73.653 404.381,83.912 Q404.381,94.414 409.569,100.728 Q414.756,107.041 423.412,107.041 Q431.296,107.041 439.656,103.52 z" fill="#555555"/>
<path d="M439.656,115.965 Q428.747,118.939 419.439,118.939 Q403.848,118.939 394.807,109.56 Q385.766,100.181 385.766,84.034 Q385.766,67.704 395.074,58.477 Q404.381,49.25 420.803,49.25 Q428.747,49.25 439.122,51.86 L439.122,64.851 Q428.332,61.27 421.87,61.27 Q413.985,61.27 409.183,67.461 Q404.381,73.653 404.381,83.912 Q404.381,94.414 409.569,100.728 Q414.756,107.041 423.412,107.041 Q431.296,107.041 439.656,103.52 z" fill-opacity="0" stroke="#555555" stroke-width="3"/>
</g>
<g>
<path d="M450.718,117.421 L450.718,27.634 L468.267,27.634 L468.267,81.849 L469.393,81.849 L492.573,51.552 L507.157,51.552 L485.755,79.299 L513.856,117.421 L492.514,117.421 L469.393,84.095 L468.267,84.095 L468.267,117.421 z" fill="#555555"/>
<path d="M450.718,117.421 L450.718,27.634 L468.267,27.634 L468.267,81.849 L469.393,81.849 L492.573,51.552 L507.157,51.552 L485.755,79.299 L513.856,117.421 L492.514,117.421 L469.393,84.095 L468.267,84.095 L468.267,117.421 z" fill-opacity="0" stroke="#555555" stroke-width="3"/>
</g>
<g>
<path d="M525.739,117.422 L525.739,27.578 L580.577,27.578 L580.577,39.902 L543.998,39.902 L543.998,64.973 L573.996,64.973 L573.996,76.992 L543.998,76.992 L543.998,104.674 L583.363,104.674 L583.363,117.422 z" fill="#555555"/>
<path d="M525.739,117.422 L525.739,27.578 L580.577,27.578 L580.577,39.902 L543.998,39.902 L543.998,64.973 L573.996,64.973 L573.996,76.992 L543.998,76.992 L543.998,104.674 L583.363,104.674 L583.363,117.422 z" fill-opacity="0" stroke="#555555" stroke-width="3"/>
</g>
<g>
<path d="M636.322,117.421 L636.322,104.856 Q629.563,118.939 615.039,118.939 Q603.3,118.939 596.601,110.137 Q589.902,101.335 589.902,85.916 Q589.902,69.161 597.461,59.206 Q605.02,49.25 617.766,49.25 Q627.963,49.25 636.322,57.506 L636.322,27.797 L653.929,27.797 L653.929,117.421 z M636.322,68.19 Q629.978,60.905 622.746,60.905 Q616.284,60.905 612.43,67.279 Q608.577,73.653 608.577,84.398 Q608.577,104.856 621.382,104.856 Q629.208,104.856 636.322,94.597 z" fill="#555555"/>
<path d="M636.322,117.421 L636.322,104.856 Q629.563,118.939 615.039,118.939 Q603.3,118.939 596.601,110.137 Q589.902,101.335 589.902,85.916 Q589.902,69.161 597.461,59.206 Q605.02,49.25 617.766,49.25 Q627.963,49.25 636.322,57.506 L636.322,27.797 L653.929,27.797 L653.929,117.421 z M636.322,68.19 Q629.978,60.905 622.746,60.905 Q616.284,60.905 612.43,67.279 Q608.577,73.653 608.577,84.398 Q608.577,104.856 621.382,104.856 Q629.208,104.856 636.322,94.597 z" fill-opacity="0" stroke="#555555" stroke-width="3"/>
</g>
<g>
<path d="M669.524,117.421 L669.524,52.526 L687.072,52.526 L687.072,117.421 z M669.524,39.78 L669.524,27.901 L687.072,27.901 L687.072,39.78 z" fill="#555555"/>
<path d="M669.524,117.421 L669.524,52.526 L687.072,52.526 L687.072,117.421 z M669.524,39.78 L669.524,27.901 L687.072,27.901 L687.072,39.78 z" fill-opacity="0" stroke="#555555" stroke-width="3"/>
</g>
<g>
<path d="M740.005,117.057 Q733.721,118.939 730.104,118.939 Q707.28,118.939 707.28,97.085 L707.28,61.998 L699.988,61.998 L699.988,50.768 L707.28,50.768 L707.28,39.537 L724.828,37.473 L724.828,50.768 L738.76,50.768 L738.76,61.998 L724.828,61.998 L724.828,94.718 Q724.828,106.98 734.61,106.98 Q736.863,106.98 740.005,106.13 z" fill="#555555"/>
<path d="M740.005,117.057 Q733.721,118.939 730.104,118.939 Q707.28,118.939 707.28,97.085 L707.28,61.998 L699.988,61.998 L699.988,50.768 L707.28,50.768 L707.28,39.537 L724.828,37.473 L724.828,50.768 L738.76,50.768 L738.76,61.998 L724.828,61.998 L724.828,94.718 Q724.828,106.98 734.61,106.98 Q736.863,106.98 740.005,106.13 z" fill-opacity="0" stroke="#555555" stroke-width="3"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -333,7 +333,9 @@ function cledit(contentElt, scrollElt, windowParam) {
editor.init = function (options) { editor.init = function (options) {
options = ({ options = ({
cursorFocusRatio: 0.2, getCursorFocusRatio: function () {
return 0.1
},
sectionHighlighter: function (section) { sectionHighlighter: function (section) {
return section.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ') return section.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ')
}, },

View File

@ -12,8 +12,6 @@ function SelectionMgr(editor) {
this.selectionStart = 0 this.selectionStart = 0
this.selectionEnd = 0 this.selectionEnd = 0
this.cursorCoordinates = {} this.cursorCoordinates = {}
this.adjustTop = 0
this.adjustBottom = 0
this.findContainer = function (offset) { this.findContainer = function (offset) {
var result = cledit.Utils.findContainer(contentElt, offset) var result = cledit.Utils.findContainer(contentElt, offset)
@ -51,18 +49,14 @@ function SelectionMgr(editor) {
this.$trigger('cursorCoordinatesChanged', coordinates) this.$trigger('cursorCoordinatesChanged', coordinates)
} }
if (adjustScroll) { if (adjustScroll) {
var adjustTop, adjustBottom var adjustment = scrollElt.clientHeight / 2 * editor.options.getCursorFocusRatio()
adjustTop = adjustBottom = scrollElt.clientHeight / 2 * editor.options.cursorFocusRatio var cursorTop = this.cursorCoordinates.top + this.cursorCoordinates.height / 2
adjustTop = this.adjustTop || adjustTop var minScrollTop = cursorTop - adjustment
adjustBottom = this.adjustBottom || adjustTop var maxScrollTop = cursorTop + adjustment - scrollElt.clientHeight
if (adjustTop && adjustBottom) { if (scrollElt.scrollTop > minScrollTop) {
var cursorMinY = scrollElt.scrollTop + adjustTop scrollElt.scrollTop = minScrollTop
var cursorMaxY = scrollElt.scrollTop + scrollElt.clientHeight - adjustBottom } else if (scrollElt.scrollTop < maxScrollTop) {
if (this.cursorCoordinates.top < cursorMinY) { scrollElt.scrollTop = maxScrollTop
scrollElt.scrollTop += this.cursorCoordinates.top - cursorMinY
} else if (this.cursorCoordinates.top + this.cursorCoordinates.height > cursorMaxY) {
scrollElt.scrollTop += this.cursorCoordinates.top + this.cursorCoordinates.height - cursorMaxY
}
} }
} }
adjustScroll = false adjustScroll = false

View File

@ -1,24 +1,43 @@
<template> <template>
<div id="app" class="app" v-bind:class="{'app--loading': loading}"> <div v-if="ready" class="app" :class="{'app--loading': loading}">
<layout></layout> <layout></layout>
<modal v-if="showModal"></modal>
</div> </div>
<div v-else class="app__spash-screen"></div>
</template> </template>
<script> <script>
import { mapState } from 'vuex';
import Layout from './Layout'; import Layout from './Layout';
import Modal from './Modal';
export default { export default {
components: { components: {
Layout, Layout,
Modal,
}, },
computed: { computed: {
...mapState([
'ready',
]),
loading() { loading() {
return !this.$store.getters['contents/current'].id; return !this.$store.getters['contents/current'].id;
}, },
showModal() {
return !!this.$store.state.modal.content;
},
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/app'; @import 'common/app';
.app__spash-screen {
position: absolute;
width: 100%;
height: 100%;
background: no-repeat center url('../assets/logo.svg');
background-color: #fff;
}
</style> </style>

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="button-bar"> <div class="button-bar">
<div class="button-bar__inner button-bar__inner--top"> <div class="button-bar__inner button-bar__inner--top">
<div class="button-bar__button" :class="{ 'button-bar__button--on': showNavigationBar }" @click="toggleNavigationBar()"> <div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showNavigationBar }" @click="toggleNavigationBar()">
<icon-navigation-bar></icon-navigation-bar> <icon-navigation-bar></icon-navigation-bar>
</div> </div>
<div class="button-bar__button" :class="{ 'button-bar__button--on': showSidePreview }" @click="toggleSidePreview()"> <div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showSidePreview }" @click="toggleSidePreview()">
<icon-side-preview></icon-side-preview> <icon-side-preview></icon-side-preview>
</div> </div>
<div class="button-bar__button" @click="toggleEditor(false)"> <div class="button-bar__button" @click="toggleEditor(false)">
@ -12,7 +12,10 @@
</div> </div>
</div> </div>
<div class="button-bar__inner button-bar__inner--bottom"> <div class="button-bar__inner button-bar__inner--bottom">
<div class="button-bar__button" :class="{ 'button-bar__button--on': showStatusBar }" @click="toggleStatusBar()"> <div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.focusMode }" @click="toggleFocusMode()">
<icon-target></icon-target>
</div>
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showStatusBar }" @click="toggleStatusBar()">
<icon-status-bar></icon-status-bar> <icon-status-bar></icon-status-bar>
</div> </div>
</div> </div>
@ -20,19 +23,18 @@
</template> </template>
<script> <script>
import { mapState, mapMutations } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
export default { export default {
computed: mapState('layout', [ computed: mapGetters('data', [
'showNavigationBar', 'localSettings',
'showSidePreview',
'showStatusBar',
]), ]),
methods: mapMutations('layout', [ methods: mapActions('data', [
'toggleNavigationBar', 'toggleNavigationBar',
'toggleEditor', 'toggleEditor',
'toggleSidePreview', 'toggleSidePreview',
'toggleStatusBar', 'toggleStatusBar',
'toggleFocusMode',
]), ]),
}; };
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="editor"> <div class="editor">
<pre class="editor__inner markdown-highlighting" :style="{'padding-left': styles.editorPadding + 'px', 'padding-right': styles.editorPadding + 'px'}"></pre> <pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}"></pre>
</div> </div>
</template> </template>
@ -26,13 +26,13 @@ export default {
} }
.editor__inner { .editor__inner {
margin: 0;
font-family: $font-family-main; font-family: $font-family-main;
font-variant-ligatures: no-common-ligatures; font-variant-ligatures: no-common-ligatures;
margin: 0;
padding: 10px 20px 360px 110px;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
caret-color: #000;
* { * {
line-height: $line-height-base; line-height: $line-height-base;

View File

@ -1,9 +1,7 @@
<template> <template>
<div class="explorer flex flex--column"> <div class="explorer flex flex--column">
<div class="side-title"> <div class="side-title flex flex--row flex--space-between">
<button class="side-title__button side-title__button--right button" @click="toggleExplorer(false)"> <div class="flex flex--row">
<icon-close></icon-close>
</button>
<button class="side-title__button button" @click="newItem()"> <button class="side-title__button button" @click="newItem()">
<icon-file-plus></icon-file-plus> <icon-file-plus></icon-file-plus>
</button> </button>
@ -17,6 +15,10 @@
<icon-delete></icon-delete> <icon-delete></icon-delete>
</button> </button>
</div> </div>
<button class="side-title__button button" @click="toggleExplorer(false)">
<icon-close></icon-close>
</button>
</div>
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" tabindex="0"> <div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" tabindex="0">
<explorer-node :node="rootNode" :depth="0"></explorer-node> <explorer-node :node="rootNode" :depth="0"></explorer-node>
</div> </div>
@ -24,10 +26,8 @@
</template> </template>
<script> <script>
import { mapState, mapGetters, mapMutations } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import ExplorerNode from './ExplorerNode'; import ExplorerNode from './ExplorerNode';
import emptyFile from '../data/emptyFile';
import emptyFolder from '../data/emptyFolder';
export default { export default {
components: { components: {
@ -42,42 +42,53 @@ export default {
]), ]),
}, },
methods: { methods: {
...mapMutations('explorer', [ ...mapActions('data', [
'setSelectedId',
]),
...mapMutations('layout', [
'toggleExplorer', 'toggleExplorer',
]), ]),
newItem(isFolder) { newItem(isFolder) {
const parentId = this.$store.getters['explorer/selectedNodeFolder'].item.id; const parentId = this.$store.getters['explorer/selectedNodeFolder'].item.id;
this.$store.dispatch('explorer/openNode', parentId); this.$store.dispatch('explorer/openNode', parentId);
this.$store.commit('explorer/setNewItem', { this.$store.commit('explorer/setNewItem', {
...isFolder ? emptyFolder() : emptyFile(), type: isFolder ? 'folder' : 'file',
parentId, parentId,
}); });
}, },
editItem() { editItem() {
const selectNode = this.$store.getters['explorer/selectedNode']; const selectedNode = this.$store.getters['explorer/selectedNode'];
this.$store.commit('explorer/setEditingId', selectNode.item.id); this.$store.commit('explorer/setEditingId', selectedNode.item.id);
}, },
deleteItem() { deleteItem() {
// const selectNode = this.$store.getters['explorer/selectedNode']; const selectedNode = this.$store.getters['explorer/selectedNode'];
// switch (this.node.item.type) { if (selectedNode.item.id) {
// case 'file': this.$store.dispatch(selectedNode.isFolder
// default: ? 'modal/folderDeletion'
// this.$store.commit('files/setCurrentId', id); : 'modal/fileDeletion',
// break; selectedNode.item)
// case 'folder': .then(() => {
// this.$store.commit('explorer/toggleOpenNode', id); if (selectedNode === this.$store.getters['explorer/selectedNode']) {
// break; if (selectedNode.isFolder) {
// } const recursiveDelete = (folderNode) => {
folderNode.folders.forEach(recursiveDelete);
folderNode.files.forEach((fileNode) => {
this.$store.commit('files/deleteItem', fileNode.item.id);
});
this.$store.commit('folders/deleteItem', folderNode.item.id);
};
recursiveDelete(selectedNode);
} else {
this.$store.commit('files/deleteItem', selectedNode.item.id);
}
}
});
}
}, },
}, },
created() { created() {
this.$store.watch( this.$store.watch(
() => this.$store.getters['files/current'].id, () => this.$store.getters['files/current'].id,
(currentFileId) => { (currentFileId) => {
this.setSelectedId(currentFileId); this.$store.commit('explorer/setSelectedId', currentFileId);
this.$store.dispatch('explorer/openNode', currentFileId);
}, { }, {
immediate: true, immediate: true,
}); });
@ -91,15 +102,27 @@ export default {
height: 100%; height: 100%;
} }
.explorer__tree {
overflow: auto;
/* fake element */
& > .explorer-node > .explorer-node__children > .explorer-node:last-child > .explorer-node__item {
height: 20px;
cursor: auto;
}
}
.side-title { .side-title {
height: 44px; height: 44px;
line-height: 44px; line-height: 44px;
padding: 4px 8px 0; padding: 4px 5px 0;
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
-webkit-flex: none;
flex: none;
} }
.side-title__button { .side-title__button {
width: 36px; width: 38px;
padding: 6px; padding: 6px;
display: inline-block; display: inline-block;
background-color: transparent; background-color: transparent;
@ -112,11 +135,6 @@ export default {
&:focus, &:focus,
&:hover { &:hover {
opacity: 1; opacity: 1;
background-color: rgba(0, 0, 0, 0.1);
} }
} }
.side-title__button--right {
float: right;
}
</style> </style>

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="explorer-node" :class="{'explorer-node--selected': selected, 'explorer-node--open': open}"> <div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.stop="onDrop">
<div v-if="editing" class="explorer-node__item-editor" :class="['explorer-node__item-editor--' + node.item.type]" :style="{'padding-left': leftPadding}"> <div v-if="isEditing" class="explorer-node__item-editor" :class="['explorer-node__item-editor--' + node.item.type]" :style="{'padding-left': leftPadding}">
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName"> <input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName">
</div> </div>
<div v-else-if="!node.isRoot" class="explorer-node__item" :class="['explorer-node__item--' + node.item.type]" :style="{'padding-left': leftPadding}" @click="select(node.item.id)"> <div v-else class="explorer-node__item" :class="['explorer-node__item--' + node.item.type]" :style="{'padding-left': leftPadding}" @click="select(node.item.id)" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
{{node.item.name}} {{node.item.name}}
</div> </div>
<div v-if="node.isFolder && open"> <div class="explorer-node__children" v-if="node.isFolder && isOpen">
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node> <explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
<div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{'padding-left': childLeftPadding}"> <div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{'padding-left': childLeftPadding}">
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keyup.enter="submitNewChild()" @keyup.esc="submitNewChild(true)" v-model.trim="newChildName"> <input type="text" class="text-input" v-focus @blur="submitNewChild()" @keyup.enter="submitNewChild()" @keyup.esc="submitNewChild(true)" v-model.trim="newChildName">
@ -17,6 +17,7 @@
</template> </template>
<script> <script>
import { mapMutations, mapActions } from 'vuex';
import utils from '../services/utils'; import utils from '../services/utils';
import defaultContent from '../data/defaultContent.md'; import defaultContent from '../data/defaultContent.md';
@ -40,13 +41,19 @@ export default {
childLeftPadding() { childLeftPadding() {
return `${(this.depth + 1) * 15}px`; return `${(this.depth + 1) * 15}px`;
}, },
selected() { isSelected() {
return this.$store.getters['explorer/selectedNode'] === this.node; return this.$store.getters['explorer/selectedNode'] === this.node;
}, },
editing() { isEditing() {
return this.$store.getters['explorer/editingNode'] === this.node; return this.$store.getters['explorer/editingNode'] === this.node;
}, },
open() { isDragTarget() {
return this.$store.getters['explorer/dragTargetNode'] === this.node;
},
isDragTargetFolder() {
return this.$store.getters['explorer/dragTargetNodeFolder'] === this.node;
},
isOpen() {
return this.$store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot; return this.$store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot;
}, },
newChild() { newChild() {
@ -71,17 +78,26 @@ export default {
}, },
}, },
methods: { methods: {
...mapMutations('explorer', [
'setDragTargetId',
]),
...mapActions('explorer', [
'setDragTarget',
]),
select(id) { select(id) {
const node = this.$store.getters['explorer/nodeMap'][id];
if (node) {
this.$store.commit('explorer/setSelectedId', id); this.$store.commit('explorer/setSelectedId', id);
if (this.node.isFolder) { if (node.isFolder) {
this.$store.commit('explorer/toggleOpenNode', id); this.$store.commit('explorer/toggleOpenNode', id);
} else { } else {
this.$store.commit('files/setCurrentId', id); this.$store.commit('files/setCurrentId', id);
} }
}
}, },
submitNewChild(cancel) { submitNewChild(cancel) {
const newChildNode = this.$store.state.explorer.newChildNode; const newChildNode = this.$store.state.explorer.newChildNode;
if (!cancel && newChildNode.item.name) { if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
const id = utils.uid(); const id = utils.uid();
if (newChildNode.isFolder) { if (newChildNode.isFolder) {
this.$store.commit('folders/setItem', { this.$store.commit('folders/setItem', {
@ -115,6 +131,33 @@ export default {
} }
this.$store.commit('explorer/setEditingId', null); this.$store.commit('explorer/setEditingId', null);
}, },
setDragSourceId(evt) {
const id = this.node.item.id;
if (id === 'fake') {
evt.preventDefault();
return;
}
this.$store.commit('explorer/setDragSourceId', id);
},
onDrop() {
const sourceNode = this.$store.getters['explorer/dragSourceNode'];
const targetNode = this.$store.getters['explorer/dragTargetNodeFolder'];
this.setDragTargetId();
if (!sourceNode.isNil
&& !targetNode.isNil
&& sourceNode.item.id !== targetNode.item.id
) {
const patch = {
id: sourceNode.item.id,
parentId: targetNode.item.id,
};
if (sourceNode.isFolder) {
this.$store.commit('folders/patchItem', patch);
} else {
this.$store.commit('files/patchItem', patch);
}
}
},
}, },
}; };
</script> </script>
@ -122,6 +165,10 @@ export default {
<style lang="scss"> <style lang="scss">
$item-font-size: 14px; $item-font-size: 14px;
.explorer-node--drag-target {
background-color: rgba(0, 128, 255, 0.2);
}
.explorer-node__item { .explorer-node__item {
cursor: pointer; cursor: pointer;
font-size: $item-font-size; font-size: $item-font-size;

View File

@ -74,7 +74,7 @@ export default {
}, },
mounted() { mounted() {
const editorElt = this.$el.querySelector('.editor__inner'); const editorElt = this.$el.querySelector('.editor__inner');
const previewElt = this.$el.querySelector('.preview__inner'); const previewElt = this.$el.querySelector('.preview__inner-2');
const tocElt = this.$el.querySelector('.toc__inner'); const tocElt = this.$el.querySelector('.toc__inner');
editorSvc.init(editorElt, previewElt, tocElt); editorSvc.init(editorElt, previewElt, tocElt);
@ -87,7 +87,7 @@ export default {
e.preventDefault(); e.preventDefault();
const y = e.clientY - tocElt.getBoundingClientRect().top; const y = e.clientY - tocElt.getBoundingClientRect().top;
this.$store.state.sectionDescList.some((sectionDesc) => { editorSvc.sectionDescList.some((sectionDesc) => {
if (y >= sectionDesc.tocDimension.endOffset) { if (y >= sectionDesc.tocDimension.endOffset) {
return false; return false;
} }
@ -124,6 +124,8 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import 'common/variables.scss';
.layout { .layout {
position: absolute; position: absolute;
width: 100%; width: 100%;

91
src/components/Modal.vue Normal file
View File

@ -0,0 +1,91 @@
<template>
<div class="modal" v-focus @keyup.esc="setContent()">
<div class="modal__inner-1">
<div class="modal__inner-2">
<div class="modal__content-text" v-html="content.text"></div>
<div class="modal__button-bar">
<button v-for="button in content.buttons" :key="button.text" class="button" @click="button.onClick()">{{button.text}}</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
directives: {
focus: {
inserted(el) {
const eltToFocus = el.querySelector('input.text-input') || el.querySelector('button.button');
if (eltToFocus) {
eltToFocus.focus();
}
},
},
},
computed: mapState('modal', [
'content',
]),
methods: {
...mapMutations('modal', [
'setContent',
]),
hideOnExternalEvent(evt) {
if (this.content) {
const modalInner = this.$el.querySelector('.modal__inner-2');
let target = evt.target;
while (target) {
if (target === modalInner) {
return;
}
target = target.parentNode;
}
this.setContent();
}
},
},
mounted() {
window.addEventListener('focusin', this.hideOnExternalEvent);
},
destroyed() {
window.removeEventListener('focusin', this.hideOnExternalEvent);
},
};
</script>
<style lang="scss">
@import 'common/variables.scss';
.modal {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(192, 192, 192, 0.8);
overflow: auto;
}
.modal__inner-1 {
margin: 0 auto;
display: table;
min-width: 320px;
max-width: 500px;
}
.modal__inner-2 {
margin: 50px 10px;
background-color: #fff;
padding: 25px 50px;
border-radius: $border-radius-base;
}
.modal__button-bar {
margin-top: 1.75rem;
text-align: right;
.button {
margin-left: 5px;
}
}
</style>

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="navigation-bar" v-bind:class="{'navigation-bar--editor': styles.showEditor}"> <div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button"> <div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
<button class="navigation-bar__button button" @click="toggleExplorer()"> <button class="navigation-bar__button button" @click="toggleExplorer()">
<icon-folder-multiple></icon-folder-multiple> <icon-folder-multiple></icon-folder-multiple>
</button> </button>
</div> </div>
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button"> <div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
<button class="navigation-bar__button button" @click="toggleExplorer()"> <button class="navigation-bar__button button" @click="toggleSideBar()">
<icon-menu></icon-menu> <icon-menu></icon-menu>
</button> </button>
</div> </div>
@ -16,7 +16,7 @@
</div> </div>
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div> <div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
<div class="navigation-bar__title navigation-bar__title--text text-input" v-bind:style="{maxWidth: styles.titleMaxWidth + 'px'}">{{title}}</div> <div class="navigation-bar__title navigation-bar__title--text text-input" v-bind:style="{maxWidth: styles.titleMaxWidth + 'px'}">{{title}}</div>
<input class="navigation-bar__title navigation-bar__title--input text-input" v-bind:class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" v-bind:style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" v-on:mouseenter="titleHover = true" v-on:mouseleave="titleHover = false" v-model="title"> <input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" v-bind:style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" v-on:mouseenter="titleHover = true" v-on:mouseleave="titleHover = false" v-model="title">
</div> </div>
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons"> <div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
<button class="navigation-bar__button button" @click="pagedownClick('bold')"> <button class="navigation-bar__button button" @click="pagedownClick('bold')">
@ -60,7 +60,7 @@
</template> </template>
<script> <script>
import { mapGetters, mapMutations } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import editorSvc from '../services/editorSvc'; import editorSvc from '../services/editorSvc';
import animationSvc from '../services/animationSvc'; import animationSvc from '../services/animationSvc';
@ -105,7 +105,7 @@ export default {
}, },
}, },
methods: { methods: {
...mapMutations('layout', [ ...mapActions('data', [
'toggleExplorer', 'toggleExplorer',
'toggleSideBar', 'toggleSideBar',
]), ]),

View File

@ -1,22 +1,35 @@
<template> <template>
<div class="preview"> <div class="preview">
<div class="preview__inner" :style="{ 'padding-left': styles.previewPadding + 'px', 'padding-right': styles.previewPadding + 'px' }"> <div class="preview__inner-1" @click="onClick" @scroll="onScroll">
<div class="preview__inner-2" :style="{padding: styles.previewPadding}">
</div>
</div>
<div v-if="!styles.showEditor" class="preview__button-bar">
<div class="preview__button" @click="toggleEditor(true)">
<icon-pen></icon-pen>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
const appUri = `${window.location.protocol}//${window.location.host}`; const appUri = `${window.location.protocol}//${window.location.host}`;
export default { export default {
data: () => ({
previewTop: true,
}),
computed: mapGetters('layout', [ computed: mapGetters('layout', [
'styles', 'styles',
]), ]),
mounted() { methods: {
this.$el.addEventListener('click', (evt) => { ...mapActions('data', [
'toggleEditor',
]),
onClick(evt) {
let elt = evt.target; let elt = evt.target;
while (elt !== this.$el) { while (elt !== this.$el) {
if (elt.href && elt.href.match(/^https?:\/\//) if (elt.href && elt.href.match(/^https?:\/\//)
@ -28,34 +41,53 @@ export default {
} }
elt = elt.parentNode; elt = elt.parentNode;
} }
}); },
onScroll(evt) {
this.previewTop = evt.target.scrollTop < 10;
},
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.preview { @import 'common/variables.scss';
.preview,
.preview__inner-1 {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
}
.preview__inner-1 {
overflow: auto; overflow: auto;
} }
.preview__inner { .preview__inner-2 {
margin: 0; margin: 0;
padding: 0 1035px 360px;
} }
.preview__inner > :first-child { .preview__inner-2 > :first-child > :first-child {
& > h1,
& > h2,
& > h3,
& > h4,
& > h5,
& > h6 {
&:first-child {
margin-top: 0; margin-top: 0;
} }
.preview__button-bar {
position: absolute;
top: 10px;
right: 26px;
}
.preview__button {
cursor: pointer;
color: rgba(0, 0, 0, 0.25);
width: 40px;
height: 40px;
padding: 5px;
border-radius: $border-radius-base;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
color: rgba(0, 0, 0, 0.75);
} }
} }
</style> </style>

View File

@ -58,7 +58,7 @@ textarea {
height: 36px; height: 36px;
padding: 3px 12px; padding: 3px 12px;
margin-bottom: 0; margin-bottom: 0;
font-size: inherit; font-size: 18px;
font-weight: 400; font-weight: 400;
line-height: 1.4; line-height: 1.4;
overflow: hidden; overflow: hidden;
@ -76,15 +76,13 @@ textarea {
border: 0; border: 0;
border-radius: $border-radius-base; border-radius: $border-radius-base;
&:focus {
color: #333;
background-color: transparent;
&:active, &:active,
& { &:focus,
&:hover {
color: #333;
background-color: rgba(0, 0, 0, 0.1);
outline: 0; outline: 0;
} }
}
} }
.flex { .flex {
@ -107,3 +105,8 @@ textarea {
flex-direction: column; flex-direction: column;
} }
.flex--space-between {
-webkit-justify-content: space-between;
justify-content: space-between;
}

View File

@ -0,0 +1,11 @@
export default () => ({
id: 'localSettings',
showNavigationBar: true,
showEditor: true,
showSidePreview: true,
showStatusBar: true,
showSideBar: false,
showExplorer: false,
focusMode: false,
updated: 0,
});

View File

@ -1,5 +1,5 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
<path d="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" /> <path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 16.8363,2.73375C 16.45,2.73375 16.0688,2.88125 15.7712,3.17375L 13.6525,5.2925L 18.955,10.5962L 21.0737,8.47625C 21.665,7.89 21.665,6.94375 21.0737,6.3575L 17.895,3.17375C 17.6025,2.88125 17.2163,2.73375 16.8363,2.73375 Z M 12.9437,6.00125L 4.84375,14.1062L 7.4025,14.39L 7.57875,16.675L 9.85875,16.85L 10.1462,19.4088L 18.2475,11.3038M 4.2475,15.0437L 2.515,21.7337L 9.19875,19.9412L 8.955,17.7838L 6.645,17.6075L 6.465,15.2925"/>
</svg> </svg>
</template> </template>

5
src/icons/Target.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00" enable-background="new 0 0 24.00 24.00" xml:space="preserve">
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 11.0013,2.0025L 11.0013,4.0725C 7.3825,4.53125 4.53125,7.3825 4.0725,11.0012L 2.0025,11.0012L 2.0025,12.9975L 4.0725,12.9975C 4.53125,16.6213 7.3825,19.4675 11.0013,19.9262L 11.0013,22.0025L 12.9975,22.0025L 12.9975,19.9313C 16.6212,19.4675 19.4675,16.6213 19.9263,12.9975L 22.0025,12.9975L 22.0025,11.0012L 19.9312,11.0012C 19.4675,7.3825 16.6212,4.53125 12.9975,4.0725L 12.9975,2.0025M 11.0013,6.08375L 11.0013,7.9975L 12.9975,7.9975L 12.9975,6.08875C 15.5175,6.51375 17.485,8.48625 17.915,11.0012L 16.0012,11.0012L 16.0012,12.9975L 17.91,12.9975C 17.485,15.5175 15.5125,17.485 12.9975,17.915L 12.9975,16.0012L 11.0013,16.0012L 11.0013,17.91C 8.48625,17.485 6.51375,15.5125 6.08375,12.9975L 7.9975,12.9975L 7.9975,11.0012L 6.08875,11.0012C 6.51375,8.48625 8.48625,6.51375 11.0013,6.08375 Z M 12.0025,11.0012C 11.445,11.0012 11.0013,11.445 11.0013,12.0025C 11.0013,12.5538 11.445,12.9975 12.0025,12.9975C 12.5537,12.9975 12.9975,12.5538 12.9975,12.0025C 12.9975,11.445 12.5537,11.0012 12.0025,11.0012 Z "/>
</svg>
</template>

View File

@ -23,6 +23,7 @@ import Delete from './Delete';
import Close from './Close'; import Close from './Close';
import FolderMultiple from './FolderMultiple'; import FolderMultiple from './FolderMultiple';
import Pen from './Pen'; import Pen from './Pen';
import Target from './Target';
Vue.component('iconFormatBold', FormatBold); Vue.component('iconFormatBold', FormatBold);
Vue.component('iconFormatItalic', FormatItalic); Vue.component('iconFormatItalic', FormatItalic);
@ -48,3 +49,4 @@ Vue.component('iconDelete', Delete);
Vue.component('iconClose', Close); Vue.component('iconClose', Close);
Vue.component('iconFolderMultiple', FolderMultiple); Vue.component('iconFolderMultiple', FolderMultiple);
Vue.component('iconPen', Pen); Vue.component('iconPen', Pen);
Vue.component('iconTarget', Target);

View File

@ -17,12 +17,12 @@ const debounce = cledit.Utils.debounce;
const allowDebounce = (action, wait) => { const allowDebounce = (action, wait) => {
let timeoutId; let timeoutId;
return (doDebounce = false) => { return (doDebounce = false, ...params) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (doDebounce) { if (doDebounce) {
timeoutId = setTimeout(() => action(), wait); timeoutId = setTimeout(() => action(...params), wait);
} else { } else {
action(); action(...params);
} }
}; };
}; };
@ -178,6 +178,12 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
this.parsingCtx = markdownConversionSvc.parseSections(this.converter, text); this.parsingCtx = markdownConversionSvc.parseSections(this.converter, text);
return this.parsingCtx.sections; return this.parsingCtx.sections;
}, },
getCursorFocusRatio: () => {
if (store.getters['data/localSettings'].focusMode) {
return 1;
}
return 0.15;
},
}; };
editorEngineSvc.initClEditor(options, reinitClEditor); editorEngineSvc.initClEditor(options, reinitClEditor);
editorEngineSvc.clEditor.toggleEditable(true); editorEngineSvc.clEditor.toggleEditable(true);
@ -321,10 +327,13 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
/** /**
* Measure the height of each section in editor, preview and toc. * Measure the height of each section in editor, preview and toc.
*/ */
measureSectionDimensions: allowDebounce(() => { measureSectionDimensions: allowDebounce((restoreScrollPosition) => {
if (editorSvc.sectionDescList && this.sectionDescList !== editorSvc.sectionDescMeasuredList) { if (editorSvc.sectionDescList && this.sectionDescList !== editorSvc.sectionDescMeasuredList) {
sectionUtils.measureSectionDimensions(editorSvc); sectionUtils.measureSectionDimensions(editorSvc);
editorSvc.sectionDescMeasuredList = editorSvc.sectionDescList; editorSvc.sectionDescMeasuredList = editorSvc.sectionDescList;
if (restoreScrollPosition) {
editorSvc.restoreScrollPosition();
}
editorSvc.$emit('sectionDescMeasuredList', editorSvc.sectionDescMeasuredList); editorSvc.$emit('sectionDescMeasuredList', editorSvc.sectionDescMeasuredList);
} }
}, 500), }, 500),
@ -538,11 +547,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
input: Object.create(editorEngineSvc.clEditor), input: Object.create(editorEngineSvc.clEditor),
}); });
this.pagedownEditor.run(); this.pagedownEditor.run();
this.editorElt.addEventListener('focus', () => {
// if (clEditorLayoutSvc.currentControl === 'menu') {
// clEditorLayoutSvc.currentControl = undefined
// }
});
// state.pagedownEditor.hooks.set('insertLinkDialog', (callback) => { // state.pagedownEditor.hooks.set('insertLinkDialog', (callback) => {
// clEditorSvc.linkDialogCallback = callback // clEditorSvc.linkDialogCallback = callback
// clEditorLayoutSvc.currentControl = 'linkDialog' // clEditorLayoutSvc.currentControl = 'linkDialog'
@ -556,14 +560,13 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
// return true // return true
// }) // })
this.editorElt.addEventListener('scroll', () => this.saveContentState(true)); this.editorElt.parentNode.addEventListener('scroll', () => this.saveContentState(true));
const refreshPreview = () => { const refreshPreview = () => {
this.convert(); this.convert();
if (instantPreview) { if (instantPreview) {
this.refreshPreview(); this.refreshPreview();
this.measureSectionDimensions(); this.measureSectionDimensions(false, true);
this.restoreScrollPosition();
} else { } else {
setTimeout(() => this.refreshPreview(), 10); setTimeout(() => this.refreshPreview(), 10);
} }
@ -704,7 +707,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
// }, 1) // }, 1)
// }) // })
// clEditorSvc.setPreviewElt(element[0].querySelector('.preview__inner')) // clEditorSvc.setPreviewElt(element[0].querySelector('.preview__inner-2'))
// var previewElt = element[0].querySelector('.preview') // var previewElt = element[0].querySelector('.preview')
// clEditorSvc.isPreviewTop = previewElt.scrollTop < 10 // clEditorSvc.isPreviewTop = previewElt.scrollTop < 10
// previewElt.addEventListener('scroll', function () { // previewElt.addEventListener('scroll', function () {
@ -742,10 +745,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
immediate: true, immediate: true,
}); });
store.watch(() => `${store.getters['layout/styles'].editorWidth},${store.getters['layout/styles'].previewWidth}`, store.watch(() => store.getters['layout/styles'],
() => editorSvc.measureSectionDimensions(true)); () => editorSvc.measureSectionDimensions(false, true));
store.watch(() => store.getters['layout/styles'].showSidePreview,
showSidePreview => showSidePreview && editorSvc.measureSectionDimensions());
}, },
}); });

View File

@ -18,7 +18,7 @@ if (window.shimIndexedDB && (!indexedDB || (navigator.userAgent.indexOf('Chrome'
function getStorePrefixFromType(type) { function getStorePrefixFromType(type) {
// Return `files` for type `file`, `folders` for type `folder`, etc... // Return `files` for type `file`, `folders` for type `folder`, etc...
const prefix = `${type}s`; const prefix = `${type}s`;
return store.state[prefix] && prefix; return store.state[prefix] ? prefix : 'data';
} }
const deleteMarkerMaxAge = 1000; const deleteMarkerMaxAge = 1000;
@ -110,7 +110,7 @@ export default {
/** /**
* Return a promise that is resolved once the synchronization between the store and the localDb * Return a promise that is resolved once the synchronization between the store and the localDb
* is finished. Effectively, open a transaction, then read and apply all changes from the DB * is finished. Effectively, open a transaction, then read and apply all changes from the DB
* since previous transaction, then write all changes from the store. * since the previous transaction, then write all the changes from the store.
*/ */
sync() { sync() {
return new Promise((resolve) => { return new Promise((resolve) => {
@ -119,10 +119,14 @@ export default {
store.state.contents, store.state.contents,
store.state.files, store.state.files,
store.state.folders, store.state.folders,
store.state.data,
].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap)); ].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap));
this.connection.createTx((tx) => { this.connection.createTx((tx) => {
this.readAll(storeItemMap, tx, () => { this.readAll(storeItemMap, tx, () => {
this.writeAll(storeItemMap, tx); this.writeAll(storeItemMap, tx);
if (!store.state.ready) {
store.commit('setReady');
}
resolve(); resolve();
}); });
}); });

View File

@ -166,13 +166,11 @@ editorSvc.$on('previewText', () => {
}); });
store.watch( store.watch(
() => store.getters['layout/styles'].showSidePreview, () => store.getters['layout/styles'],
(showSidePreview) => { () => {
if (showSidePreview) {
isScrollEditor = true; isScrollEditor = true;
isScrollPreview = false; isScrollPreview = false;
skipAnimation = true; skipAnimation = true;
}
}); });
store.watch( store.watch(

View File

@ -4,22 +4,34 @@ import Vuex from 'vuex';
import contents from './modules/contents'; import contents from './modules/contents';
import files from './modules/files'; import files from './modules/files';
import folders from './modules/folders'; import folders from './modules/folders';
import data from './modules/data';
import layout from './modules/layout'; import layout from './modules/layout';
import editor from './modules/editor'; import editor from './modules/editor';
import explorer from './modules/explorer'; import explorer from './modules/explorer';
import modal from './modules/modal';
Vue.use(Vuex); Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
const store = new Vuex.Store({ const store = new Vuex.Store({
state: {
ready: false,
},
mutations: {
setReady: (state) => {
state.ready = true;
},
},
modules: { modules: {
contents, contents,
files, files,
folders, folders,
data,
layout, layout,
editor, editor,
explorer, explorer,
modal,
}, },
strict: debug, strict: debug,
plugins: debug ? [createLogger()] : [], plugins: debug ? [createLogger()] : [],

41
src/store/modules/data.js Normal file
View File

@ -0,0 +1,41 @@
import moduleTemplate from './moduleTemplate';
import defaultLocalSettings from '../../data/defaultLocalSettings';
const localSettingsToggler = propertyName => ({ getters, dispatch }, value) => {
dispatch('patchLocalSettings', {
[propertyName]: value === undefined ? !getters.localSettings[propertyName] : value,
});
};
const module = moduleTemplate((id) => {
switch (id) {
case 'localSettings':
return defaultLocalSettings();
default:
throw new Error(`Unknown data id ${id}`);
}
});
module.getters = {
...module.getters,
localSettings: state => state.itemMap.localSettings || defaultLocalSettings(),
};
module.actions = {
...module.actions,
patchLocalSettings({ getters, commit }, value) {
commit('patchOrSetItem', {
...value,
id: 'localSettings',
});
},
toggleNavigationBar: localSettingsToggler('showNavigationBar'),
toggleEditor: localSettingsToggler('showEditor'),
toggleSidePreview: localSettingsToggler('showSidePreview'),
toggleStatusBar: localSettingsToggler('showStatusBar'),
toggleSideBar: localSettingsToggler('showSideBar'),
toggleExplorer: localSettingsToggler('showExplorer'),
toggleFocusMode: localSettingsToggler('focusMode'),
};
export default module;

View File

@ -6,6 +6,14 @@ const setter = propertyName => (state, value) => {
state[propertyName] = value; state[propertyName] = value;
}; };
function debounceAction(action, wait) {
let timeoutId;
return (context) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => action(context), wait);
};
}
const collator = new Intl.Collator(undefined, { sensitivity: 'base' }); const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
const compare = (node1, node2) => collator.compare(node1.item.name, node2.item.name); const compare = (node1, node2) => collator.compare(node1.item.name, node2.item.name);
@ -31,19 +39,29 @@ class Node {
const nilFileNode = new Node(emptyFile()); const nilFileNode = new Node(emptyFile());
nilFileNode.isNil = true; nilFileNode.isNil = true;
const fakeFileNode = new Node(emptyFile());
fakeFileNode.item.id = 'fake';
function getParent(node, getters) { function getParent(node, getters) {
if (node === nilFileNode) { if (node.isNil) {
return nilFileNode; return nilFileNode;
} }
return getters.nodeMap[node.item.parentId] || getters.rootNode; return getters.nodeMap[node.item.parentId] || getters.rootNode;
} }
function getFolder(node, getters) {
return node.item.type === 'folder' ?
node :
getParent(node, getters);
}
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
selectedId: null, selectedId: null,
editingId: null, editingId: null,
dragSourceId: null,
dragTargetId: null,
newChildNode: nilFileNode, newChildNode: nilFileNode,
openNodes: {}, openNodes: {},
}, },
@ -70,6 +88,8 @@ export default {
} }
}); });
rootNode.sortChildren(); rootNode.sortChildren();
// Add a fake file at the end of the root folder to always allow drag and drop into it.
rootNode.files.push(fakeFileNode);
return { return {
nodeMap, nodeMap,
rootNode, rootNode,
@ -79,19 +99,29 @@ export default {
rootNode: (state, getters) => getters.nodeStructure.rootNode, rootNode: (state, getters) => getters.nodeStructure.rootNode,
newChildNodeParent: (state, getters) => getParent(state.newChildNode, getters), newChildNodeParent: (state, getters) => getParent(state.newChildNode, getters),
selectedNode: (state, getters) => getters.nodeMap[state.selectedId] || nilFileNode, selectedNode: (state, getters) => getters.nodeMap[state.selectedId] || nilFileNode,
selectedNodeFolder: (state, getters) => { selectedNodeFolder: (state, getters) => getFolder(getters.selectedNode, getters),
const selectedNode = getters.selectedNode;
return selectedNode.item.type === 'folder'
? selectedNode
: getParent(selectedNode, getters);
},
editingNode: (state, getters) => getters.nodeMap[state.editingId] || nilFileNode, editingNode: (state, getters) => getters.nodeMap[state.editingId] || nilFileNode,
dragSourceNode: (state, getters) => getters.nodeMap[state.dragSourceId] || nilFileNode,
dragTargetNode: (state, getters) => {
if (state.dragTargetId === 'fake') {
return fakeFileNode;
}
return getters.nodeMap[state.dragTargetId] || nilFileNode;
},
dragTargetNodeFolder: (state, getters) => {
if (state.dragTargetId === 'fake') {
return getters.rootNode;
}
return getFolder(getters.dragTargetNode, getters);
},
}, },
mutations: { mutations: {
setSelectedId: setter('selectedId'), setSelectedId: setter('selectedId'),
setEditingId: setter('editingId'), setEditingId: setter('editingId'),
setDragSourceId: setter('dragSourceId'),
setDragTargetId: setter('dragTargetId'),
setNewItem(state, item) { setNewItem(state, item) {
state.newChildNode = item ? new Node(item) : nilFileNode; state.newChildNode = item ? new Node(item, item.type === 'folder') : nilFileNode;
}, },
setNewItemName(state, name) { setNewItemName(state, name) {
state.newChildNode.item.name = name; state.newChildNode.item.name = name;
@ -110,5 +140,12 @@ export default {
dispatch('openNode', node.item.parentId); dispatch('openNode', node.item.parentId);
} }
}, },
openDragTarget: debounceAction(({ state, dispatch }) => {
dispatch('openNode', state.dragTargetId);
}, 1000),
setDragTarget({ state, getters, commit, dispatch }, id) {
commit('setDragTargetId', id);
dispatch('openDragTarget');
},
}, },
}; };

View File

@ -1,5 +1,7 @@
const editorMinWidth = 280; const editorMinWidth = 280;
const minPadding = 20; const minPadding = 20;
const previewButtonWidth = 55;
const editorTopPadding = 10;
const navigationBarSpaceWidth = 30; const navigationBarSpaceWidth = 30;
const navigationBarLeftWidth = 570; const navigationBarLeftWidth = 570;
const maxTitleMaxWidth = 800; const maxTitleMaxWidth = 800;
@ -9,10 +11,6 @@ const setter = propertyName => (state, value) => {
state[propertyName] = value; state[propertyName] = value;
}; };
const toggler = propertyName => (state, value) => {
state[propertyName] = value === undefined ? !state[propertyName] : value;
};
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
@ -23,26 +21,12 @@ export default {
buttonBarWidth: 30, buttonBarWidth: 30,
statusBarHeight: 20, statusBarHeight: 20,
}, },
// Configuration
showNavigationBar: true,
showEditor: true,
showSidePreview: true,
showStatusBar: true,
showSideBar: false,
showExplorer: true,
editorWidthFactor: 1, editorWidthFactor: 1,
fontSizeFactor: 1, fontSizeFactor: 1,
// Styles
bodyWidth: 0, bodyWidth: 0,
bodyHeight: 0, bodyHeight: 0,
}, },
mutations: { mutations: {
toggleNavigationBar: toggler('showNavigationBar'),
toggleEditor: toggler('showEditor'),
toggleSidePreview: toggler('showSidePreview'),
toggleStatusBar: toggler('showStatusBar'),
toggleSideBar: toggler('showSideBar'),
toggleExplorer: toggler('showExplorer'),
setEditorWidthFactor: setter('editorWidthFactor'), setEditorWidthFactor: setter('editorWidthFactor'),
setFontSizeFactor: setter('fontSizeFactor'), setFontSizeFactor: setter('fontSizeFactor'),
updateBodySize: (state) => { updateBodySize: (state) => {
@ -51,15 +35,16 @@ export default {
}, },
}, },
getters: { getters: {
styles: (state) => { styles: (state, getters, rootState, rootGetters) => {
const localSettings = rootGetters['data/localSettings'];
const styles = { const styles = {
showNavigationBar: !state.showEditor || state.showNavigationBar, showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
showStatusBar: state.showStatusBar, showStatusBar: localSettings.showStatusBar,
showEditor: state.showEditor, showEditor: localSettings.showEditor,
showSidePreview: state.showSidePreview && state.showEditor, showSidePreview: localSettings.showSidePreview && localSettings.showEditor,
showPreview: state.showSidePreview || !state.showEditor, showPreview: localSettings.showSidePreview || !localSettings.showEditor,
showSideBar: state.showSideBar, showSideBar: localSettings.showSideBar,
showExplorer: state.showExplorer, showExplorer: localSettings.showExplorer,
}; };
function computeStyles() { function computeStyles() {
@ -96,6 +81,7 @@ export default {
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) { if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
styles.showSidePreview = false; styles.showSidePreview = false;
styles.showPreview = false;
computeStyles(); computeStyles();
return; return;
} }
@ -118,15 +104,24 @@ export default {
} }
styles.fontSize *= state.fontSizeFactor; styles.fontSize *= state.fontSizeFactor;
const panelWidth = doublePanelWidth / 2; const bottomPadding = Math.floor(styles.innerHeight / 2);
const panelWidth = Math.floor(doublePanelWidth / 2);
styles.previewWidth = styles.showSidePreview ? styles.previewWidth = styles.showSidePreview ?
panelWidth : panelWidth :
styles.innerWidth; styles.innerWidth;
styles.previewPadding = Math.max((styles.previewWidth - styles.textWidth) / 2, minPadding); const previewLeftPadding = Math.max(
Math.floor((styles.previewWidth - styles.textWidth) / 2), minPadding);
let previewRightPadding = previewLeftPadding;
if (!styles.showEditor && previewRightPadding < previewButtonWidth) {
previewRightPadding = previewButtonWidth;
}
styles.previewPadding = `${editorTopPadding}px ${previewRightPadding}px ${bottomPadding}px ${previewLeftPadding}px`;
styles.editorWidth = styles.showSidePreview ? styles.editorWidth = styles.showSidePreview ?
panelWidth : panelWidth :
doublePanelWidth; doublePanelWidth;
styles.editorPadding = Math.max((styles.editorWidth - styles.textWidth) / 2, minPadding); const editorSidePadding = Math.max(
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
styles.titleMaxWidth = styles.innerWidth - navigationBarSpaceWidth; styles.titleMaxWidth = styles.innerWidth - navigationBarSpaceWidth;
if (styles.showEditor) { if (styles.showEditor) {

View File

@ -0,0 +1,49 @@
const confirmButtons = yesText => [{
text: 'No',
}, {
text: yesText || 'Yes',
resolve: true,
}];
export default {
namespaced: true,
state: {
content: null,
},
mutations: {
setContent: (state, value) => {
state.content = value;
},
},
actions: {
open({ commit }, content) {
return new Promise((resolve, reject) => {
if (!content.buttons) {
content.buttons = [{
text: 'OK',
resolve: true,
}];
}
content.buttons.forEach((button) => {
button.onClick = () => {
commit('setContent');
if (button.resolve) {
resolve(button.resolve);
} else {
reject();
}
};
});
commit('setContent', content);
});
},
fileDeletion: ({ dispatch }, item) => dispatch('open', {
text: `<p>You are about to delete the file <b>${item.name}</b>. Are you sure ?</p>`,
buttons: confirmButtons('Yes, delete'),
}),
folderDeletion: ({ dispatch }, item) => dispatch('open', {
text: `<p>You are about to delete the folder <b>${item.name}</b> and all its files. Are you sure ?</p>`,
buttons: confirmButtons('Yes, delete'),
}),
},
};

View File

@ -1,7 +1,26 @@
import Vue from 'vue'; import Vue from 'vue';
import utils from '../../services/utils';
export default empty => ({ export default (empty) => {
function setItem(state, value) {
const item = Object.assign(empty(value.id), value);
if (!item.updated) {
item.updated = Date.now();
}
Vue.set(state.itemMap, item.id, item);
}
function patchItem(state, patch) {
const item = state.itemMap[patch.id];
if (item) {
Object.assign(item, patch);
item.updated = Date.now(); // Trigger sync
Vue.set(state.itemMap, item.id, item);
return true;
}
return false;
}
return {
namespaced: true, namespaced: true,
state: { state: {
itemMap: {}, itemMap: {},
@ -10,22 +29,11 @@ export default empty => ({
items: state => Object.keys(state.itemMap).map(key => state.itemMap[key]), items: state => Object.keys(state.itemMap).map(key => state.itemMap[key]),
}, },
mutations: { mutations: {
setItem(state, value) { setItem,
const item = Object.assign(empty(), value); patchItem,
if (!item.id) { patchOrSetItem(state, patch) {
item.id = utils.uid(); if (!patchItem(state, patch)) {
} setItem(state, patch);
if (!item.updated) {
item.updated = Date.now();
}
Vue.set(state.itemMap, item.id, item);
},
patchItem(state, patch) {
const item = state.itemMap[patch.id];
if (item) {
Object.assign(item, patch);
item.updated = Date.now(); // Trigger sync
Vue.set(state.itemMap, item.id, item);
} }
}, },
deleteItem(state, id) { deleteItem(state, id) {
@ -33,4 +41,5 @@ export default empty => ({
}, },
}, },
actions: {}, actions: {},
}); };
};