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:
parent
b0d2a5e071
commit
7d97bb1cae
119
static/components/CategoryItem.js
Normal file
119
static/components/CategoryItem.js
Normal 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]
|
||||
}
|
120
static/components/CategoryItemDescription.js
Normal file
120
static/components/CategoryItemDescription.js
Normal 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
|
134
static/components/CategoryItemInfo.js
Normal file
134
static/components/CategoryItemInfo.js
Normal 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>
|
||||
`,
|
||||
});
|
Loading…
Reference in New Issue
Block a user