1
1
mirror of https://github.com/aelve/guide.git synced 2024-12-24 21:35:06 +03:00

Unfinished Vue rewrites of several components

This commit is contained in:
Artyom Kazak 2018-09-01 14:29:54 +02:00
parent b0d2a5e071
commit 7d97bb1cae
3 changed files with 373 additions and 0 deletions

View File

@ -0,0 +1,119 @@
////////////////////////////////////////////////////////////////////////////
// A single item on a category page.
//
// For now, each item is a separate Vue.js "app", and stuff like moving
// items up and down is done by modifying DOM with jQuery. As we keep
// rewriting the frontend, jQuery would be replaced by editing the order
// of items in the category object.
////////////////////////////////////////////////////////////////////////////
Vue.component('CategoryItem', {
props: {
/** Contents of the item */
item: {type: Object, required: true},
/** Item hue (depends on some data in the category, hence not part of
* the item) */
hue: {type: Number, required: true},
/** The category defines some sections to be visible or hidden */
enabledTraits: {type: Boolean, required: true},
enabledEcosystem: {type: Boolean, required: true},
enabledNotes: {type: Boolean, required: true},
},
data: function() { return {
/** Whether to show the item info edit form */
editing: false,
}},
computed: {
/** DOM identifier for our item */
itemNode: function() {
return "item-" + this.item.uid },
},
methods: {
/**
* When the "up"/"down" control is clicked, move the item one position
* up/down, both on the backend and the frontend.
*/
onMoveUp() {
$.post("/haskell/move/item/" + this.item.uid, {direction: "up"})
.done(() => { moveNodeUp('#' + this.itemNode);
fadeIn('#' + this.itemNode); });
},
onMoveDown() {
$.post("/haskell/move/item/" + this.item.uid, {direction: "down"})
.done(() => { moveNodeDown('#' + this.itemNode);
fadeIn('#' + this.itemNode); });
},
/**
* When the "delete" control is clicked, notify the backend and then
* remove the item entirely.
*/
onDelete() {
if (confirm("Confirm deletion?")) {
$.post("/haskell/delete/item/" + this.item.uid)
.done(() => { fadeOutAndRemove('#' + this.itemNode); });
}
},
},
template: `
<div :id="itemNode" class="item">
<div class="item-info" :style="{ backgroundColor: darkColor(this.hue) }">
<CategoryItemInfo v-if="!editing"
@move-item-up="onMoveUp"
@move-item-down="onMoveDown"
@delete-item="onDelete"
@edit-item-info="editing=true"
/>
<CategoryItemInfoEdit v-if="editing"
@cancel-edit="editing=false"/>
</div>
<div class="item-body" :style="{ backgroundColor: lightColor(this.hue) }">
<CategoryItemDescription/> ???
<CategoryItemTraits v-if="enabledTraits"/>
<CategoryItemEcosystem v-if="enabledEcosystem"/>
<CategoryItemNotes v-if="enabledNotes"/>
</div>
</div>
`,
});
////////////////////////////////////////////////////////////////////////////
// Hues used to color items. For each hue (integer) there is a light color
// and a dark color. -1 stands for a colorless item.
//
// The colors were taken from Google's color palette at
// https://www.google.com/design/spec/style/color.html#color-color-palette
// ("100" for dark and "50" for light), except for gray which didn't really
// fit.
////////////////////////////////////////////////////////////////////////////
/**
* @param {Number} hue Integer number (or -1 for "colorless")
* @returns {String} Color code, e.g. "#D1C4E9"
*/
function darkColor(hue) {
// deep purple, green, amber, blue,
// red, brown, teal, lime
var colors = [
"#D1C4E9", "#C8E6C9", "#FFECB3", "#BBDEFB",
"#FFCDD2", "#D7CCC8", "#B2DFDB", "#F0F4C3"];
if (hue == -1) return "#D6D6D6"; else return colors[hue % 8]
}
/**
* @param {Number} hue Integer number (or -1 for "colorless")
* @returns {String} Color code, e.g. "#D1C4E9"
*/
function lightColor(hue) {
// deep purple, green, amber, blue,
// red, brown, teal, limehau
var colors = [
"#EDE7F6", "#E8F5E9", "#FFF8E1", "#E3F2FD",
"#FFEBEE", "#EFEBE9", "#E0F2F1", "#F9FBE7"];
if (hue == -1) return "#F0F0F0"; else return colors[hue % 8]
}

View File

@ -0,0 +1,120 @@
////////////////////////////////////////////////////////////////////////////
// Item description (also called "Summary")
//
// Provides a way to edit the description. If the content changed on the backend
// in the meantime, opens a dialog and offers to resolve potential merge
// conflicts.
////////////////////////////////////////////////////////////////////////////
Vue.component('CategoryItemDescription', {
props: {
/** The ID of the item that the description belongs to */
itemId: {type: String, required: true},
/** Description itself, contains `text` and `html` */
initContent: {type: Object, required: true},
},
data: function() { return {
/**
* `editing` signals whether we're in editing mode
* */
editing: false,
/**
* `unsaved` signals whether the editor might have unsaved changes. If
* this is the case, we hide it instead of destroying it. When the user
* clicks "Cancel", their changes get lost; when they click the pencil
* button, they remain.
* */
unsaved: false,
/**
* `startingContent` is the "starting point" of editing. When we submit,
* the backend does a three-way diff between the description as we've
* last seen it (i.e. `startingContent`), our proposed edited
* description, and the one that the backend currently has.
* */
startingContent: initContent,
}},
computed: {
/** DOM identifier for our item */
itemNode: function() {
return "item-" + this.itemId },
},
methods: {
/**
* Submit description to the backend and, if it changed on the backend
* in the meantime, show a merge resolution popup. If the content
* changes *again* while the user resolves the merge, we'll show another
* popup, etc.
*
* TODO: it would be nice to replace `startingContent` when we submit
* the merged version, but we don't get rendered Markdown from the
* backend, so we can't do that.
* */
submitDescription(startingContentText, editedContentText) {
$.post({
url: `/haskell/set/item/${this.itemId}/description`,
data: {
original: startingContentText,
content: editedContentText },
success: (data) => {
$.magnificPopup.close();
this.editing = false;
this.unsaved = false;
this.startingContent = data; },
statusCode: {
409: (xhr, st, err) => {
/** Current item description on the backend */
let backendText = xhr.responseJSON["modified"];
/** Backend's proposed merge (the diff between our
* 'startingContentText' and 'editedContentText, applied
* to 'backendText') */
let proposedMerge = xhr.responseJSON["merged"];
showDiffPopup(startingContentText, backendText, proposedMerge,
(ourMerge) => {
// Now the user looked at the `backendText` and
// (hopefully) applied their edits to it, so
// `backendText` becomes the new starting point
this.submitDescription(backendText, ourMerge) }); } },
});
},
},
// TODO: we don't need 'section' anymore, I think. Also, there's a bit of
// duplication in this template.
//
// TODO: check that when an editor opens, it gets focus (everywhere we use
// an editor)
template: `
<div class="item-description">
<div v-if="!editing" class="section normal">
<strong>Summary</strong>
<span style="margin-left:0.5em"></span>
<a href="#" class="small-control edit-item-description"
@click.prevent="editing=true; unsaved=true;">
<img src="/pencil.svg" title="edit summary"></a>
<div class="notes-like">
<template v-if="content.text != ''" v-html="content.html"></template>
<p v-else>write something here!</p>
</div>
</div>
<div v-if="editing || unsaved" v-show="editing" class="section editing">
<strong>Summary</strong>
<span style="margin-left:0.5em"></span>
<a href="#" class="small-control edit-item-description"
@click.prevent="editing=false; unsaved=true;">
<img src="/pencil.svg" title="hide the editor"></a>
<AEditor
:init-content="content.text"
:instruction="'or press Ctrl+Enter to save'"
@submit-edit="(e) => submitDescription(startingContent.text, e)"
@cancel-edit="editing=false; unsaved=false;"
/>
</div>
</div>
`,
});
// rows=10 should go somewhere

View File

@ -0,0 +1,134 @@
////////////////////////////////////////////////////////////////////////////
// Information about the item, along with some controls
//
// Events:
// * move-item-up User wants to move the item up
// * mode-item-down User wants to move the item down
// * delete-item User wants to delete the item
// * edit-item-info User wants to edit item info
//
////////////////////////////////////////////////////////////////////////////
Vue.component('CategoryItemInfo', {
props: {
// Contents of the item
item: {type: Object, required: true},
},
computed: {
// Item group, e.g. "PCRE-based" or "POSIX-based" for regex libraries
group: function() {
return this.item.group_ || "other" },
// Link to the "official site" or something
link: function () {
return this.item.link || null },
// Link to the Hackage page (for Haskell packages)
hackageLink: function () {
return (this.item.kind.tag == "Library") ? this.item.kind.contents :
(this.item.kind.tag == "Tool") ? this.item.kind.contents :
null },
},
// TODO: this template is somewhat messy; styles should be moved out;
// spans and divs used like here are weird, too
template: `
<div>
<div style="font-size:23px; line-height:27px;">
<a class="anchor" :href="item.link_to_item">#</a>
</div>
<div style="font-size:23px; line-height:27px;">
<a v-if="link !== null" :href="link" class="item-name">{{ item.name }}</a>
<span v-else>{{ item.name }}</span>
<template v-if="hackageLink !== null">
(<a :href="hackageLink">Hackage</a>)
</template>
</div>
<div class="item-group" style="line-height:27px;">
{{ group }}
</div>
<div class="controls">
<span>
<a href="#" class="move-item-up"
@click.prevent="this.$emit('move-item-up')">
<img src="/arrow-thick-top.svg" alt="up" title="move item up"></a>
<a href="#" class="move-item-down"
@click.prevent="this.$emit('move-item-down')">
<img src="/arrow-thick-bottom.svg" alt="down" title="move item down"></a>
</span>
<span>
<a href="#" class="edit-item-info"
@click.prevent="this.$emit('edit-item-info')">
<img src="/cog.svg" alt="edit" title="edit item info"></a>
<span style="margin-left: 0.4em"></span>
<a href="#" class="delete-item"
@click.prevent="this.$emit('delete-item')">
<img src="/x.svg" alt="delete" title="delete item"></a>
</span>
</div>
`,
});
////////////////////////////////////////////////////////////////////////////
// Item info edit form
//
// Shown when you press the cog in the item titlebar.
////////////////////////////////////////////////////////////////////////////
Vue.component('CategoryItemInfoEdit', {
props: {
// Contents of the item
item: {type: Object, required: true},
},
// We use 'autocomplete=off' everywhere due to this:
// http://stackoverflow.com/q/8311455
template: `
<form class="item-info-edit-form" onsubmit="submitItemInfo('{{item.uid.uidToText}}', this); return false;">
<label for="name">Name</label>
<input id="name" name="name" value="{{item.name}}"
type="text" autocomplete="off">
<label for="kind">Kind</label>
<select id="kind" name="kind" autocomplete="off">
{{! possible_kinds would have stuff like library, tool, other }}
{{#possible_kinds}}
<option value="{{name}}" {{%selectIf selected}}>{{caption}}</option>
{{/possible_kinds}}
</select>
<label for="hackage-name">Name on Hackage</label>
<input id="hackage-name" name="hackage-name" value="{{#item.kind.hackageName}}{{.}}{{/item.kind.hackageName}}"
type="text" autocomplete="off">
<label for="site">Site (optional)</label>
<input id="site" name="link" value="{{item.link}}"
type="text" autocomplete="off">
<div class="form-group">
<label for="group">Group</label>
{{! When new group is selected in the list, we show a field for
entering new group's name }}
<select id="group" name="group" onchange="itemGroupSelectHandler(this);"
autocomplete="off">
<option value="-" {{%selectIf item_no_group}}>-</option>
{{# category_groups }}
<option value="{{name}}" {{%selectIf selected}}>{{name}}</option>
{{/ category_groups }}
<option value="">New group...</option>
</select>
<input hidden class="custom-group-input" name="custom-group"
type="text" autocomplete="off">
</div>
<div class="form-btn-group">
<input value="Save" class="save" type="submit">
<input value="Cancel" class="cancel" type="button"
onclick="itemInfoCancelEdit('{{item.uid.uidToText}}');">
</div>
</form>
`,
});