mirror of https://github.com/fmartingr/shiori.git
Update UI
This commit is contained in:
parent
d01f87b358
commit
5232e0e2c9
File diff suppressed because one or more lines are too long
|
@ -97,18 +97,11 @@ func (h *webHandler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, p
|
|||
err = json.NewDecoder(r.Body).Decode(&book)
|
||||
checkError(err)
|
||||
|
||||
// Make sure URL valid
|
||||
parsedURL, err := nurl.ParseRequestURI(book.URL)
|
||||
if err != nil || parsedURL.Host == "" {
|
||||
panic(fmt.Errorf("URL is not valid"))
|
||||
}
|
||||
|
||||
// Clear UTM parameters from URL
|
||||
book.URL = clearUTMParams(parsedURL)
|
||||
|
||||
// Fetch data from internet
|
||||
article, _ := readability.Parse(book.URL, 10*time.Second)
|
||||
article, err := readability.Parse(book.URL, 20*time.Second)
|
||||
checkError(err)
|
||||
|
||||
book.URL = article.URL
|
||||
book.ImageURL = article.Meta.Image
|
||||
book.Author = article.Meta.Author
|
||||
book.MinReadTime = article.Meta.MinReadTime
|
||||
|
@ -116,18 +109,9 @@ func (h *webHandler) apiInsertBookmark(w http.ResponseWriter, r *http.Request, p
|
|||
book.Content = article.Content
|
||||
book.HTML = article.RawContent
|
||||
|
||||
// If title and excerpt doesnt have submitted value, use from article
|
||||
if book.Title == "" {
|
||||
book.Title = article.Meta.Title
|
||||
}
|
||||
|
||||
if book.Excerpt == "" {
|
||||
book.Excerpt = article.Meta.Excerpt
|
||||
}
|
||||
|
||||
// Make sure title is not empty
|
||||
if book.Title == "" {
|
||||
book.Title = "Untitled"
|
||||
book.Title = book.URL
|
||||
}
|
||||
|
||||
// Save to database
|
||||
|
|
|
@ -28,6 +28,8 @@ func newWebHandler(db dt.Database) (*webHandler, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
jwtKey = []byte("Test12345")
|
||||
|
||||
// Create templates
|
||||
funcMap := template.FuncMap{
|
||||
"html": func(s string) template.HTML {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
.header-link{border-right:1px solid #E5E5E5;color:#232323;cursor:pointer;font-size:.9em;line-height:60px;overflow:hidden;padding:0 16px}.header-link:hover{color:#F44336}.full-overlay{position:fixed;z-index:101;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column;background-color:rgba(0,0,0,0.5);top:0;left:0;right:0;bottom:0;overflow:hidden;-webkit-box-pack:center;justify-content:center;padding:32px}.yla-dialog__overlay{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;min-width:0;min-height:0;overflow:hidden;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:10001;background-color:rgba(0,0,0,0.6);padding:32px}.yla-dialog__overlay .yla-dialog{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column nowrap;min-width:400px;min-height:0;max-height:100%;overflow:hidden;background-color:#FFF;font-size:16px}.yla-dialog__overlay .yla-dialog>.yla-dialog__header{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row nowrap;padding:16px;min-height:0;background-color:#353535;color:#EEE;flex-shrink:0}.yla-dialog__overlay .yla-dialog>.yla-dialog__header>p{-webkit-box-flex:1;flex:1 0;font-weight:600;font-size:1em;text-transform:uppercase}.yla-dialog__overlay .yla-dialog>.yla-dialog__header>a:hover{color:#F44336}.yla-dialog__overlay .yla-dialog>.yla-dialog__body{padding:16px;display:grid;max-height:100%;min-height:80px;min-width:0;font-size:.9em;overflow:auto;grid-template-columns:max-content 1fr;-webkit-box-align:baseline;align-items:baseline;grid-gap:16px}.yla-dialog__overlay .yla-dialog>.yla-dialog__body>.yla-dialog__content{grid-column-start:1;grid-column-end:3;align-self:baseline}.yla-dialog__overlay .yla-dialog>.yla-dialog__body>input{color:#232323;padding:8px;border:1px solid #E5E5E5}.yla-dialog__overlay .yla-dialog>.yla-dialog__body>.suggestion{position:absolute;display:block;padding:8px;background-color:#EEE;border:1px solid #E5E5E5}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer{padding:16px;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;-webkit-box-pack:end;justify-content:flex-end;border-top:1px solid #E5E5E5}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer>a{text-transform:uppercase;padding:0 8px;font-size:.9em;font-weight:600}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer>a:hover{color:#F44336}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer>a:focus{outline:none;color:#F44336;border-bottom:1px dashed #F44336}.yla-dialog__overlay .yla-dialog>.yla-dialog__footer>i{width:19px;line-height:19px;text-align:center}
|
|
@ -0,0 +1 @@
|
|||
.header-link{border-right:1px solid #E5E5E5;color:#232323;cursor:pointer;font-size:.9em;line-height:70px;overflow:hidden;padding:0 16px}.header-link:hover{color:#F44336}.full-overlay{position:fixed;z-index:101;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-flow:column;background-color:rgba(0,0,0,0.5);top:0;left:0;right:0;bottom:0;overflow:hidden;-webkit-box-pack:center;justify-content:center;padding:32px}.yla-tooltip{font-size:14px;color:#EEE;background-color:#232323;padding:8px;border-radius:4px;position:relative;z-index:1000}.yla-tooltip::after{content:'';display:block;position:absolute;border:8px solid transparent}.yla-tooltip.left{margin-left:-14px}.yla-tooltip.left::after{top:50%;right:-16px;margin-top:-8px;border-left-color:#232323}.yla-tooltip.top{margin-top:-14px}.yla-tooltip.top::after{left:50%;bottom:-16px;margin-left:-8px;border-top-color:#232323}.yla-tooltip.right{margin-left:14px}.yla-tooltip.right::after{top:50%;left:-16px;margin-top:-8px;border-right-color:#232323}.yla-tooltip.bottom{margin-top:14px}.yla-tooltip.bottom::after{left:50%;top:-16px;margin-left:-8px;border-bottom-color:#232323}
|
740
view/index.html
740
view/index.html
|
@ -5,12 +5,17 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<link rel="stylesheet" href="/css/stylesheet.css">
|
||||
<link rel="stylesheet" href="/css/fontawesome.css">
|
||||
<link rel="stylesheet" href="/css/source-sans-pro.css">
|
||||
<link rel="stylesheet" href="/css/yla-dialog.css">
|
||||
<link rel="stylesheet" href="/css/yla-tooltip.css">
|
||||
<link rel="stylesheet" href="/css/stylesheet.css">
|
||||
<script src="/js/vue.js"></script>
|
||||
<script src="/js/axios.js"></script>
|
||||
<script src="/js/js-cookie.js"></script>
|
||||
<script src="/js/component/yla-tooltip.js"></script>
|
||||
<script src="/js/component/yla-dialog.js"></script>
|
||||
<script src="/js/page/base.js"></script>
|
||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/res/apple-touch-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/res/apple-touch-icon-152x152.png" />
|
||||
<link rel="icon" type="image/png" href="/res/favicon-32x32.png" sizes="32x32" />
|
||||
|
@ -19,653 +24,146 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<div id="main-page">
|
||||
<div id="header">
|
||||
<template v-if="checkedBookmarks.length === 0">
|
||||
<a id="logo" href="/">
|
||||
<span>栞</span>shiori</a>
|
||||
<div id="search-box">
|
||||
<input type="text" name="keyword" v-model.trim="search.query" @keyup.enter="loadData" placeholder="Search url, tags, title or content">
|
||||
<a class="button" @click="loadData">
|
||||
<i class="fas fa-search fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div id="header-menu" v-if="!loading">
|
||||
<a @click="reloadData">
|
||||
<i class="fas fa-cloud fa-fw"></i>
|
||||
<span>Reload</span>
|
||||
</a>
|
||||
<a @click="showTagCloud">
|
||||
<i class="fas fa-hashtag fa-fw"></i>
|
||||
<span>Tags</span>
|
||||
</a>
|
||||
<a @click="toggleImage">
|
||||
<i class="fas fa-fw" :class="showImage ? 'fa-eye-slash' : 'fa-eye'"></i>
|
||||
<span>{{showImage ? 'Hide image' : 'Show image'}}</span>
|
||||
</a>
|
||||
<a @click="logout">
|
||||
<i class="fas fa-sign-out-alt fa-fw"></i>
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p id="n-selected">{{checkedBookmarks.length}} selected</p>
|
||||
<div id="header-menu">
|
||||
<a @click="clearSelectedBookmarks">
|
||||
<i class="fas fa-fw fa-ban"></i>
|
||||
<span>Cancel</span>
|
||||
</a>
|
||||
<a @click="selectAllBookmarks">
|
||||
<i class="fas fa-fw fa-check-square"></i>
|
||||
<span>Select all</span>
|
||||
</a>
|
||||
<a @click="deleteBookmarks(checkedBookmarks)">
|
||||
<i class="fas fa-fw fa-trash"></i>
|
||||
<span>Delete</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<div id="index-page">
|
||||
<div id="sidebar">
|
||||
<p id="logo">栞</p>
|
||||
<yla-tooltip placement="right" content="Reload data">
|
||||
<a>
|
||||
<i class="fas fa-sync-alt fa-fw"></i>
|
||||
</a>
|
||||
</yla-tooltip>
|
||||
<yla-tooltip placement="right" content="Add new bookmark">
|
||||
<a @click="showDialogAdd">
|
||||
<i class="fas fa-plus fa-fw"></i>
|
||||
</a>
|
||||
</yla-tooltip>
|
||||
<yla-tooltip placement="right" content="Batch edit">
|
||||
<a>
|
||||
<i class="fas fa-pencil-alt fa-fw"></i>
|
||||
</a>
|
||||
</yla-tooltip>
|
||||
<div class="spacer"></div>
|
||||
<yla-tooltip placement="right" content="Toggle night mode">
|
||||
<a>
|
||||
<i class="fas fa-moon fa-fw"></i>
|
||||
</a>
|
||||
</yla-tooltip>
|
||||
<yla-tooltip placement="right" content="Log out">
|
||||
<a>
|
||||
<i class="fas fa-sign-out-alt fa-fw"></i>
|
||||
</a>
|
||||
</yla-tooltip>
|
||||
</div>
|
||||
<div id="main">
|
||||
<template v-if="!loading && error === ''">
|
||||
<div id="input-bookmark">
|
||||
<p v-if="inputBookmark.url !== ''">{{inputBookmark.id === -1 ? 'New bookmark' : 'Edit bookmark'}}</p>
|
||||
<input type="text" ref="inputURL" v-model.trim="inputBookmark.url" placeholder="URL for the new bookmark" @focus="clearSelectedBookmarks">
|
||||
<template v-if="inputBookmark.url !== ''">
|
||||
<input type="text" v-model.trim="inputBookmark.title" placeholder="Custom bookmark title (optional)">
|
||||
<input type="text" v-model.trim="inputBookmark.tags" placeholder="Space separated tags for this bookmark (optional)">
|
||||
<textarea name="excerpt" v-model.trim="inputBookmark.excerpt" placeholder="Excerpt for this bookmark (optional)"></textarea>
|
||||
<p v-if="inputBookmark.error !== ''" class="error-message">{{inputBookmark.error}}</p>
|
||||
<div class="button-area">
|
||||
<div class="spacer"></div>
|
||||
<a v-if="inputBookmark.loading">
|
||||
<i class="fas fa-fw fa-spinner fa-spin"></i>
|
||||
</a>
|
||||
<template v-else>
|
||||
<a class="button" @click="clearInputBookmark">Cancel</a>
|
||||
<a class="button" @click="saveBookmark">Done</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="search.query !== '' && !loading" id="search-parameter">
|
||||
<a v-if="search.keyword !== ''" @click="removeSearchParam(search.keyword)">{{search.keyword}}</a>
|
||||
<a v-for="tag in search.tags" @click="removeSearchParam('#'+tag)">#{{tag}}</a>
|
||||
</div>
|
||||
<div id="grid">
|
||||
<div v-for="column in gridColumns" class="column" :style="{maxWidth: columnWidth}">
|
||||
<div v-for="item in column" class="bookmark" :class="{checked: isBookmarkChecked(item.index)}" :ref="'bookmark-'+item.index">
|
||||
<a class="checkbox" @click="toggleBookmarkCheck(item.index)">
|
||||
<i class="fas fa-check"></i>
|
||||
</a>
|
||||
<a class="bookmark-metadata" target="_blank" :class="{'has-image':bookmarkImage(item) !== ''}" :style="bookmarkImage(item)" :href="item.url">
|
||||
<p class="bookmark-time">{{bookmarkTime(item)}}</p>
|
||||
<p class="bookmark-title">{{item.title}}</p>
|
||||
<p class="bookmark-url">{{getDomainURL(item.url)}}</p>
|
||||
</a>
|
||||
<p v-if="item.excerpt !== ''" class="bookmark-excerpt">{{item.excerpt}}</p>
|
||||
<div v-if="item.tags.length > 0" class="bookmark-tags">
|
||||
<a v-for="tag in item.tags" @click="searchTag(tag.name)">{{tag.name}}</a>
|
||||
</div>
|
||||
<div class="bookmark-menu">
|
||||
<a @click="updateBookmark(item.index)">
|
||||
<i class="fas fa-sync"></i>
|
||||
<span>Update</span>
|
||||
</a>
|
||||
<a @click="editBookmark(item.index)">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
<span>Edit</span>
|
||||
</a>
|
||||
<a @click="deleteBookmarks([item.index])">
|
||||
<i class="far fa-trash-alt"></i>
|
||||
<span>Delete</span>
|
||||
</a>
|
||||
<a :href="'/bookmark/'+item.id" target="_blank">
|
||||
<i class="fas fa-history"></i>
|
||||
<span>Cache</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="body">
|
||||
<div id="header">
|
||||
<input type="text" placeholder="Search bookmarks by url, tags, title or content">
|
||||
<a>
|
||||
<i class="fas fa-search fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div id="grid">
|
||||
<div class="bookmark" v-for="book in bookmarks">
|
||||
<a class="bookmark-content" :href="'/bookmark/'+book.id" target="_blank">
|
||||
<img v-if="book.imageURL !== ''" :src="book.imageURL">
|
||||
<p class="title">{{book.title}}</p>
|
||||
<p class="excerpt" v-if="book.imageURL === ''">{{book.excerpt}}</p>
|
||||
</a>
|
||||
<div class="bookmark-menu">
|
||||
<a class="url" title="View original" :href="book.url" target="_blank">
|
||||
{{getHostname(book.url)}}
|
||||
</a>
|
||||
<a title="Edit bookmark">
|
||||
<i class="fas fa-pencil-alt"></i>
|
||||
</a>
|
||||
<a title="Edit tags">
|
||||
<i class="fas fa-tags"></i>
|
||||
</a>
|
||||
<a title="Delete bookmark">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</a>
|
||||
<a title="Update cache">
|
||||
<i class="fas fa-cloud-download-alt"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="loading || error !== ''" id="message-bar">
|
||||
<i v-if="loading" class="fas fa-fw fa-spinner fa-spin"></i>
|
||||
<p v-if="error !== ''" class="error-message">{{error}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="dialog.visible" id="dialog-overlay">
|
||||
<div id="dialog">
|
||||
<p id="dialog-title" :class="{'error-message': dialog.isError}">{{dialog.title}}</p>
|
||||
<p v-html="dialog.content" id="dialog-content"></p>
|
||||
<div id="dialog-button">
|
||||
<div class="spacer"></div>
|
||||
<a v-if="dialog.loading">
|
||||
<i class="fas fa-fw fa-spinner fa-spin"></i>
|
||||
</a>
|
||||
<template v-else>
|
||||
<a class="button" @click="dialog.secondAction">{{dialog.secondChoice}}</a>
|
||||
<a class="button" @click="dialog.mainAction">{{dialog.mainChoice}}</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tagCloud.visible" id="tag-cloud-overlay">
|
||||
<div id="tag-cloud">
|
||||
<div id="tag-cloud-title">
|
||||
<p>Tag Cloud</p>
|
||||
<a @click="tagCloud.visible = false" id="close-tag-cloud">
|
||||
<i class="fas fa-fw fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div id="tag-cloud-content">
|
||||
<a v-for="item in tagCloud.data" @click="selectTagCloud(item.name)" :style="{fontSize: item.fontSize+'em'}">#{{item.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<yla-dialog v-bind="dialog"></yla-dialog>
|
||||
</div>
|
||||
<script>
|
||||
// Prepare axios instance
|
||||
var token = Cookies.get('token'),
|
||||
instance = axios.create();
|
||||
rest = axios.create();
|
||||
|
||||
instance.defaults.timeout = 10000;
|
||||
instance.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
||||
rest.defaults.timeout = 15000;
|
||||
rest.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
||||
|
||||
var app = new Vue({
|
||||
el: '#main-page',
|
||||
// Register Vue component
|
||||
Vue.component('yla-dialog', new YlaDialog());
|
||||
Vue.component('yla-tooltip', new YlaTooltip());
|
||||
|
||||
new Vue({
|
||||
el: '#index-page',
|
||||
mixins: [new Base()],
|
||||
data: {
|
||||
windowWidth: 0,
|
||||
error: "",
|
||||
loading: false,
|
||||
displayTags: false,
|
||||
bookmarks: [],
|
||||
tags: [],
|
||||
checkedBookmarks: [],
|
||||
showImage: true,
|
||||
search: {
|
||||
query: "",
|
||||
keyword: "",
|
||||
tags: []
|
||||
},
|
||||
inputBookmark: {
|
||||
index: -1,
|
||||
id: -1,
|
||||
url: "",
|
||||
title: "",
|
||||
tags: "",
|
||||
excerpt: "",
|
||||
error: "",
|
||||
loading: false
|
||||
},
|
||||
dialog: {
|
||||
visible: false,
|
||||
loading: false,
|
||||
isError: false,
|
||||
title: '',
|
||||
content: '',
|
||||
mainChoice: '',
|
||||
secondChoice: '',
|
||||
mainAction: function () {},
|
||||
secondAction: function () {}
|
||||
},
|
||||
tagCloud: {
|
||||
visible: false,
|
||||
data: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
searchTag: function (tag) {
|
||||
if (this.loading) return;
|
||||
|
||||
var newTag = '#' + tag;
|
||||
if (this.search.query.indexOf(newTag) === -1) {
|
||||
this.search.query += ' ' + newTag;
|
||||
this.search.query = this.search.query.trim().replace(/\s+/g, ' ');
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
removeSearchParam: function (param) {
|
||||
if (this.loading) return;
|
||||
this.search.query = this.search.query.replace(param, ' ').trim().replace(/\s+/g, ' ');
|
||||
this.loadData();
|
||||
},
|
||||
reloadData: function () {
|
||||
if (this.loading) return;
|
||||
this.search.query = '';
|
||||
this.loadData();
|
||||
},
|
||||
loadData: function () {
|
||||
if (this.loading) return;
|
||||
|
||||
// Parse search query
|
||||
var rxTags = /(^|\s+)#(\S+)/g,
|
||||
tags = [];
|
||||
|
||||
while ((result = rxTags.exec(this.search.query)) !== null) {
|
||||
tags.push(result[2]);
|
||||
}
|
||||
|
||||
var keyword = this.search.query.replace(/(^|\s+)#(\S+)/g, ' ').trim().replace(/\s+/g, ' ');
|
||||
|
||||
// Fetch data
|
||||
this.error = '';
|
||||
this.loading = true;
|
||||
this.search.tags = tags;
|
||||
this.search.keyword = keyword;
|
||||
instance.get('/api/bookmarks', {
|
||||
params: {
|
||||
keyword: this.search.keyword,
|
||||
tags: this.search.tags.join(" ")
|
||||
}
|
||||
})
|
||||
.then(function (response) {
|
||||
app.loading = false;
|
||||
app.bookmarks = response.data;
|
||||
})
|
||||
.catch(function (error) {
|
||||
var errorMsg = error.response ? error.response.data : error.message;
|
||||
app.loading = false;
|
||||
app.error = errorMsg.trim();
|
||||
});
|
||||
},
|
||||
saveBookmark: function () {
|
||||
if (this.inputBookmark.loading) return;
|
||||
this.inputBookmark.loading = true;
|
||||
|
||||
if (this.inputBookmark.url === "") return;
|
||||
|
||||
var idx = this.inputBookmark.index,
|
||||
tags = this.inputBookmark.tags.replace(/\s+/g, " "),
|
||||
newTags = tags === "" ? [] : listTag = tags.split(/\s+/g),
|
||||
finalTags = [];
|
||||
|
||||
if (idx !== -1) {
|
||||
var oldTags = this.bookmarks[idx].tags;
|
||||
for (var i = 0; i < oldTags.length; i++) {
|
||||
if (newTags.indexOf(oldTags[i].name) === -1) {
|
||||
finalTags.push({
|
||||
name: '-' + oldTags[i].name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < newTags.length; i++) {
|
||||
finalTags.push({
|
||||
name: listTag[i]
|
||||
});
|
||||
}
|
||||
|
||||
instance.request({
|
||||
method: this.inputBookmark.id === -1 ? 'post' : 'put',
|
||||
url: '/api/bookmarks',
|
||||
timeout: 15000,
|
||||
data: {
|
||||
id: this.inputBookmark.id,
|
||||
url: this.inputBookmark.url,
|
||||
title: this.inputBookmark.title,
|
||||
excerpt: this.inputBookmark.excerpt,
|
||||
tags: finalTags
|
||||
}
|
||||
})
|
||||
.then(function (response) {
|
||||
if (idx === -1) app.bookmarks.unshift(response.data);
|
||||
else {
|
||||
app.bookmarks.splice(idx, 1, response.data);
|
||||
app.bookmarks[idx].tags.splice(0, app.bookmarks[idx].tags.length, ...response.data.tags);
|
||||
}
|
||||
|
||||
app.clearInputBookmark();
|
||||
})
|
||||
.catch(function (error) {
|
||||
var errorMsg = error.response ? error.response.data : error.message;
|
||||
app.inputBookmark.loading = false;
|
||||
app.inputBookmark.error = errorMsg.trim();
|
||||
});
|
||||
},
|
||||
editBookmark: function (idx) {
|
||||
var bookmark = this.bookmarks[idx],
|
||||
tags = [];
|
||||
|
||||
for (var i = 0; i < bookmark.tags.length; i++) {
|
||||
tags.push(bookmark.tags[i].name);
|
||||
}
|
||||
|
||||
this.inputBookmark.index = idx;
|
||||
this.inputBookmark.id = bookmark.id;
|
||||
this.inputBookmark.url = bookmark.url;
|
||||
this.inputBookmark.title = bookmark.title;
|
||||
this.inputBookmark.tags = tags.join(" ");
|
||||
this.inputBookmark.excerpt = bookmark.excerpt;
|
||||
|
||||
this.$nextTick(function () {
|
||||
window.scrollTo(0, 0);
|
||||
app.$refs.inputURL.focus();
|
||||
});
|
||||
},
|
||||
deleteBookmarks: function (indices) {
|
||||
var title = "Delete Bookmarks",
|
||||
content = "Delete the selected bookmark(s) ? This action is irreversible.",
|
||||
smallestIndex = 1;
|
||||
|
||||
if (indices.length === 0) return;
|
||||
else if (indices.length === 1) {
|
||||
var bookmark = this.bookmarks[indices[0]];
|
||||
|
||||
smallestIndex = indices[0];
|
||||
title = "Delete Bookmark";
|
||||
content = "Delete <b>\"" + bookmark.title.trim() + "\"</b> from bookmarks ? This action is irreversible.";
|
||||
} else {
|
||||
indices.sort();
|
||||
smallestIndex = indices[indices.length - 1];
|
||||
}
|
||||
|
||||
this.dialog.visible = true;
|
||||
this.dialog.isError = false;
|
||||
this.dialog.loading = false;
|
||||
this.dialog.title = title;
|
||||
this.dialog.content = content;
|
||||
this.dialog.mainChoice = "Yes";
|
||||
this.dialog.secondChoice = "No";
|
||||
this.dialog.mainAction = function () {
|
||||
app.dialog.loading = true;
|
||||
|
||||
var listId = [];
|
||||
for (var i = 0; i < indices.length; i++) {
|
||||
listId.push('' + app.bookmarks[indices[i]].id);
|
||||
}
|
||||
|
||||
instance.delete('/api/bookmarks/', {
|
||||
data: listId
|
||||
})
|
||||
.then(function (response) {
|
||||
app.dialog.loading = false;
|
||||
app.dialog.visible = false;
|
||||
|
||||
for (var i = indices.length - 1; i >= 0; i--) {
|
||||
app.bookmarks.splice(indices[i], 1);
|
||||
}
|
||||
app.clearSelectedBookmarks();
|
||||
|
||||
var scrollIdx = smallestIndex === 1 ? 1 : smallestIndex - 1;
|
||||
app.$nextTick(function () {
|
||||
var el = app.$refs['bookmark-' + smallestIndex][0];
|
||||
if (el) el.scrollIntoView();
|
||||
else window.scrollTo(0, 0);
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
var errorMsg = error.response ? error.response.data : error.message;
|
||||
app.showDialogError("Error Deleting Bookmark", errorMsg.trim());
|
||||
});
|
||||
};
|
||||
this.dialog.secondAction = function () {
|
||||
app.dialog.visible = false;
|
||||
app.$nextTick(function () {
|
||||
app.$refs['bookmark-' + smallestIndex][0].scrollIntoView();
|
||||
});
|
||||
};
|
||||
|
||||
},
|
||||
updateBookmark: function (idx) {
|
||||
var bookmark = this.bookmarks[idx],
|
||||
sendUpdateRequest = function (overwrite) {
|
||||
var url = "/api/bookmarks";
|
||||
if (!overwrite) url += "?dont-overwrite";
|
||||
|
||||
instance.put(url, {
|
||||
id: bookmark.id,
|
||||
excerpt: overwrite ? "empty" : bookmark.excerpt
|
||||
}, {
|
||||
timeout: 15000,
|
||||
})
|
||||
.then(function (response) {
|
||||
app.dialog.loading = false;
|
||||
app.dialog.visible = false;
|
||||
app.bookmarks.splice(idx, 1, response.data);
|
||||
app.bookmarks[idx].tags.splice(0, app.bookmarks[idx].tags.length, ...response.data.tags);
|
||||
})
|
||||
.catch(function (error) {
|
||||
var errorMsg = error.response ? error.response.data : error.message;
|
||||
app.showDialogError("Error Updating Bookmark", errorMsg.trim());
|
||||
});
|
||||
};
|
||||
|
||||
this.dialog.visible = true;
|
||||
this.dialog.isError = false;
|
||||
this.dialog.loading = false;
|
||||
this.dialog.title = "Update Bookmark";
|
||||
this.dialog.content = "Update data of <b>\"" + bookmark.title.trim() + "\"</b> ? This action is irreversible.";
|
||||
this.dialog.mainChoice = "Yes";
|
||||
this.dialog.secondChoice = "No";
|
||||
this.dialog.mainAction = function () {
|
||||
app.dialog.title = "Overwrite Metadata";
|
||||
app.dialog.content = "Overwrite the existing bookmark's metadata ?";
|
||||
app.dialog.mainChoice = "Yes";
|
||||
app.dialog.secondChoice = "No";
|
||||
app.dialog.mainAction = function () {
|
||||
app.dialog.loading = true;
|
||||
sendUpdateRequest(true);
|
||||
};
|
||||
app.dialog.secondAction = function () {
|
||||
app.dialog.loading = true;
|
||||
sendUpdateRequest(false);
|
||||
};
|
||||
};
|
||||
this.dialog.secondAction = function () {
|
||||
app.dialog.visible = false;
|
||||
app.$nextTick(function () {
|
||||
app.$refs['bookmark-' + idx][0].scrollIntoView();
|
||||
});
|
||||
};
|
||||
},
|
||||
toggleBookmarkCheck: function (idx) {
|
||||
var checkedIdx = this.checkedBookmarks.indexOf(idx);
|
||||
if (checkedIdx !== -1) this.checkedBookmarks.splice(checkedIdx, 1);
|
||||
else this.checkedBookmarks.push(idx);
|
||||
},
|
||||
selectAllBookmarks: function () {
|
||||
this.clearSelectedBookmarks();
|
||||
for (var i = 0; i < this.bookmarks.length; i++) {
|
||||
this.checkedBookmarks.push(i);
|
||||
}
|
||||
},
|
||||
clearSelectedBookmarks: function () {
|
||||
this.checkedBookmarks.splice(0, this.checkedBookmarks.length);
|
||||
},
|
||||
isBookmarkChecked: function (idx) {
|
||||
return this.checkedBookmarks.indexOf(idx) !== -1;
|
||||
},
|
||||
clearInputBookmark: function () {
|
||||
var idx = this.inputBookmark.index;
|
||||
|
||||
this.inputBookmark.index = -1;
|
||||
this.inputBookmark.id = -1;
|
||||
this.inputBookmark.url = "";
|
||||
this.inputBookmark.title = "";
|
||||
this.inputBookmark.tags = "";
|
||||
this.inputBookmark.excerpt = "";
|
||||
this.inputBookmark.error = "";
|
||||
this.inputBookmark.loading = false;
|
||||
|
||||
if (idx !== -1) app.$nextTick(function () {
|
||||
var bookmarkItem = app.$refs['bookmark-' + idx];
|
||||
bookmarkItem[0].scrollIntoView();
|
||||
})
|
||||
},
|
||||
showTagCloud: function () {
|
||||
loadData() {
|
||||
if (this.loading) return;
|
||||
|
||||
// Fetch data
|
||||
this.error = '';
|
||||
this.loading = true;
|
||||
instance.get('/api/tags')
|
||||
.then(function (response) {
|
||||
app.loading = false;
|
||||
app.tagCloud.data = response.data;
|
||||
|
||||
if (app.tagCloud.data.length === 0) {
|
||||
app.tagCloud.visible = false;
|
||||
app.showDialogError("Error Creating Tag Cloud", "There are no saved tags");
|
||||
return;
|
||||
}
|
||||
|
||||
app.tagCloud.visible = true;
|
||||
|
||||
// Find largest tags frequency
|
||||
var minFont = 1,
|
||||
maxFont = 5,
|
||||
maxCount = 0;
|
||||
for (var i = 0; i < app.tagCloud.data.length; i++) {
|
||||
if (app.tagCloud.data[i].nBookmarks > maxCount) {
|
||||
maxCount = app.tagCloud.data[i].nBookmarks;
|
||||
}
|
||||
}
|
||||
|
||||
// Set font size for tags
|
||||
for (var i = 0; i < app.tagCloud.data.length; i++) {
|
||||
var size = (Math.log(app.tagCloud.data[i].nBookmarks) / Math.log(maxCount)) *
|
||||
(maxFont - minFont) + minFont;
|
||||
app.tagCloud.data[i].fontSize = size;
|
||||
}
|
||||
rest.get('/api/bookmarks')
|
||||
.then((response) => {
|
||||
this.loading = false;
|
||||
this.bookmarks = response.data;
|
||||
console.log(JSON.stringify(response.data));
|
||||
})
|
||||
.catch(function (error) {
|
||||
var errorMsg = error.response ? error.response.data : error.message;
|
||||
app.loading = false;
|
||||
app.tagCloud.visible = false;
|
||||
app.showDialogError("Error Creating Tag Cloud", errorMsg.trim());
|
||||
.catch((error) => {
|
||||
var errorMsg = (error.response ? error.response.data : error.message).trim();
|
||||
this.loading = false;
|
||||
this.showErrorDialog(errorMsg);
|
||||
});
|
||||
},
|
||||
selectTagCloud: function (tag) {
|
||||
this.tagCloud.visible = false;
|
||||
this.searchTag(tag);
|
||||
},
|
||||
bookmarkTime: function (book) {
|
||||
// Define read time
|
||||
var readTime = "";
|
||||
if (book.maxReadTime === 0) {
|
||||
readTime = "";
|
||||
} else if (book.minReadTime === book.maxReadTime) {
|
||||
readTime = book.minReadTime + " min read";
|
||||
} else {
|
||||
readTime = book.minReadTime + "-" + book.maxReadTime + " min read";
|
||||
}
|
||||
|
||||
// Convert modified time to local
|
||||
var time = new Date(book.modified.replace(/-/g, '/') + ' +00');
|
||||
|
||||
// Create final time
|
||||
var month = ("00" + (time.getMonth() + 1)).slice(-2),
|
||||
date = ("00" + time.getDate()).slice(-2),
|
||||
hours = ("00" + time.getHours()).slice(-2),
|
||||
minutes = ("00" + time.getMinutes()).slice(-2),
|
||||
seconds = ("00" + time.getSeconds()).slice(-2);
|
||||
|
||||
var finalBookmarkTime = "Updated " +
|
||||
time.getFullYear() + "-" + month + "-" + date + " " +
|
||||
hours + ":" + minutes + ":" + seconds;
|
||||
|
||||
if (readTime !== "") finalBookmarkTime += " \u00B7 " + readTime;
|
||||
|
||||
return finalBookmarkTime;
|
||||
},
|
||||
toggleImage: function () {
|
||||
this.showImage = !this.showImage;
|
||||
if (this.showImage) localStorage.setItem('show-image', '');
|
||||
else localStorage.removeItem('show-image');
|
||||
},
|
||||
bookmarkImage: function (book) {
|
||||
if (!this.showImage) return "";
|
||||
if (book.imageURL === "") return "";
|
||||
return "background-image: url(" + book.imageURL + ")";
|
||||
},
|
||||
getDomainURL: function (url) {
|
||||
var hostname;
|
||||
|
||||
if (url.indexOf("://") > -1) {
|
||||
hostname = url.split('/')[2];
|
||||
} else {
|
||||
hostname = url.split('/')[0];
|
||||
}
|
||||
|
||||
hostname = hostname.split(':')[0];
|
||||
hostname = hostname.split('?')[0];
|
||||
|
||||
return hostname;
|
||||
},
|
||||
showDialogError: function (title, msg) {
|
||||
this.dialog.isError = true;
|
||||
this.dialog.visible = true;
|
||||
this.dialog.loading = false;
|
||||
this.dialog.title = title;
|
||||
this.dialog.content = msg;
|
||||
this.dialog.mainChoice = "OK"
|
||||
this.dialog.secondChoice = ""
|
||||
this.dialog.mainAction = function () {
|
||||
app.dialog.visible = false;
|
||||
}
|
||||
this.dialog.secondAction = function () {}
|
||||
},
|
||||
logout: function () {
|
||||
Cookies.remove('token');
|
||||
location.href = '/login';
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
overlayVisible: function () {
|
||||
return this.dialog.visible || this.tagCloud.visible;
|
||||
},
|
||||
gridColumns: function () {
|
||||
var nColumn = Math.round(this.windowWidth / 500),
|
||||
finalContent = [],
|
||||
currentColumn = 0;
|
||||
|
||||
for (var i = 0; i < nColumn; i++) {
|
||||
finalContent.push([]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.bookmarks.length; i++) {
|
||||
var bookmark = this.bookmarks[i];
|
||||
bookmark.index = i;
|
||||
finalContent[currentColumn].push(bookmark);
|
||||
|
||||
currentColumn += 1;
|
||||
if (currentColumn >= nColumn) currentColumn = 0;
|
||||
}
|
||||
|
||||
return finalContent;
|
||||
},
|
||||
columnWidth: function () {
|
||||
var nColumn = Math.round(this.windowWidth / 500),
|
||||
percent = Math.round(100 / nColumn * 1000) / 1000;
|
||||
|
||||
return percent + "%";
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
overlayVisible: function (isVisible) {
|
||||
if (isVisible) document.body.className = "noscroll";
|
||||
else document.body.removeAttribute("class");
|
||||
},
|
||||
'inputBookmark.url': function (newURL) {
|
||||
if (newURL === "") this.clearInputBookmark();
|
||||
else this.$nextTick(function () {
|
||||
app.$refs.inputURL.focus();
|
||||
showDialogAdd() {
|
||||
this.showDialog({
|
||||
title: 'New Bookmark',
|
||||
content: 'Save an URL to bookmark',
|
||||
fields: [{
|
||||
name: 'url',
|
||||
label: 'http://...',
|
||||
}],
|
||||
mainText: 'OK',
|
||||
secondText: 'Cancel',
|
||||
mainClick: (data) => {
|
||||
this.dialog.loading = true;
|
||||
rest.post('/api/bookmarks', {
|
||||
url: data.url,
|
||||
})
|
||||
.then((response) => {
|
||||
this.dialog.loading = false;
|
||||
this.dialog.visible = false;
|
||||
this.bookmarks.unshift(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
var errorMsg = (error.response ? error.response.data : error.message).trim();
|
||||
this.showErrorDialog(errorMsg);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
getHostname(url) {
|
||||
parser = document.createElement('a');
|
||||
parser.href = url;
|
||||
return parser.hostname;
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.showImage = localStorage.getItem('show-image') !== null;
|
||||
this.windowWidth = window.innerWidth;
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
app.windowWidth = window.innerWidth;
|
||||
})
|
||||
|
||||
mounted() {
|
||||
this.loadData();
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
var YlaDialog = function () {
|
||||
// Private variable
|
||||
var _template = `
|
||||
<div v-if="visible" class="yla-dialog__overlay">
|
||||
<div class="yla-dialog">
|
||||
<div class="yla-dialog__header">
|
||||
<p>{{title}}</p>
|
||||
</div>
|
||||
<div class="yla-dialog__body">
|
||||
<slot>
|
||||
<p class="yla-dialog__content">{{content}}</p>
|
||||
<template v-for="(field,index) in formFields">
|
||||
<p v-if="showLabel">{{field.label}} :</p>
|
||||
<input :style="{gridColumnEnd: showLabel ? null : 'span 2'}"
|
||||
:type="fieldType(field)"
|
||||
:placeholder="field.label"
|
||||
:tabindex="index+1"
|
||||
ref="input"
|
||||
v-model="field.value"
|
||||
@focus="$event.target.select()"
|
||||
@keyup="handleInput(index)"
|
||||
@keyup.enter="handleInputEnter(index)">
|
||||
<span ref="suggestion" v-if="field.suggestion" class="suggestion">{{field.suggestion}}</span>
|
||||
</template>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="yla-dialog__footer">
|
||||
<i v-if="loading" class="fas fa-fw fa-spinner fa-spin"></i>
|
||||
<slot v-else name="custom-footer">
|
||||
<a v-if="secondText"
|
||||
:tabindex="btnTabIndex+1"
|
||||
@click="handleSecondClick"
|
||||
@keyup.enter="handleSecondClick"
|
||||
class="yla-dialog__button">{{secondText}}
|
||||
</a>
|
||||
<a :tabindex="btnTabIndex"
|
||||
ref="mainButton"
|
||||
@click="handleMainClick"
|
||||
@keyup.enter="handleMainClick"
|
||||
class="yla-dialog__button main">{{mainText}}
|
||||
</a>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
return {
|
||||
template: _template,
|
||||
props: {
|
||||
visible: Boolean,
|
||||
loading: Boolean,
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
default () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
mainText: {
|
||||
type: String,
|
||||
default: 'OK'
|
||||
},
|
||||
secondText: String,
|
||||
mainClick: {
|
||||
type: Function,
|
||||
default () {}
|
||||
},
|
||||
secondClick: {
|
||||
type: Function,
|
||||
default () {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formFields: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
btnTabIndex() {
|
||||
return this.fields.length + 1;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fields: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.formFields = this.fields.map(field => {
|
||||
if (typeof field === 'string') return {
|
||||
name: field,
|
||||
label: field,
|
||||
value: '',
|
||||
type: 'text',
|
||||
dictionary: [],
|
||||
suggestion: undefined
|
||||
}
|
||||
|
||||
if (typeof field === 'object') return {
|
||||
name: field.name || '',
|
||||
label: field.label || '',
|
||||
value: field.value || '',
|
||||
type: field.type || 'text',
|
||||
dictionary: field.dictionary instanceof Array ? field.dictionary : [],
|
||||
suggestion: undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
'fields.length' () {
|
||||
this.focus();
|
||||
},
|
||||
visible() {
|
||||
this.focus();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fieldType(f) {
|
||||
var type = f.type || 'text';
|
||||
if (type !== 'text' && type !== 'password') return 'text';
|
||||
else return type;
|
||||
},
|
||||
handleMainClick() {
|
||||
var data = {};
|
||||
this.formFields.forEach(field => {
|
||||
var value = field.value;
|
||||
if (field.type === 'number') value = parseInt(value, 10) || 0;
|
||||
else if (field.type === 'float') value = parseFloat(value) || 0.0;
|
||||
data[field.name] = value;
|
||||
})
|
||||
this.mainClick(data);
|
||||
},
|
||||
handleSecondClick() {
|
||||
this.secondClick();
|
||||
},
|
||||
handleInput(index) {
|
||||
// Create initial variable
|
||||
var field = this.formFields[index],
|
||||
dictionary = field.dictionary;
|
||||
|
||||
// Make sure dictionary is not empty
|
||||
if (dictionary.length === 0) return;
|
||||
|
||||
// Fetch suggestion from dictionary
|
||||
var words = field.value.split(' '),
|
||||
lastWord = words[words.length - 1].toLowerCase(),
|
||||
suggestion;
|
||||
|
||||
if (lastWord !== '') {
|
||||
suggestion = dictionary.find(word => {
|
||||
return word.toLowerCase().startsWith(lastWord)
|
||||
});
|
||||
}
|
||||
|
||||
this.formFields[index].suggestion = suggestion;
|
||||
|
||||
// Make sure suggestion exist
|
||||
if (suggestion == null) return;
|
||||
|
||||
// Display suggestion
|
||||
this.$nextTick(() => {
|
||||
var input = this.$refs.input[index],
|
||||
span = this.$refs.suggestion[index],
|
||||
inputRect = input.getBoundingClientRect();
|
||||
|
||||
span.style.top = (inputRect.bottom - 1) + 'px';
|
||||
span.style.left = inputRect.left + 'px';
|
||||
});
|
||||
},
|
||||
handleInputEnter(index) {
|
||||
var suggestion = this.formFields[index].suggestion;
|
||||
|
||||
if (suggestion == null) {
|
||||
this.handleMainClick();
|
||||
return;
|
||||
}
|
||||
|
||||
var words = this.formFields[index].value.split(' ');
|
||||
words.pop();
|
||||
words.push(suggestion);
|
||||
|
||||
this.formFields[index].value = words.join(' ') + ' ';
|
||||
this.formFields[index].suggestion = undefined;
|
||||
},
|
||||
focus() {
|
||||
this.$nextTick(() => {
|
||||
if (!this.visible) return;
|
||||
|
||||
var fields = this.$refs.input,
|
||||
otherInput = this.$el.querySelectorAll('input'),
|
||||
button = this.$refs.mainButton;
|
||||
|
||||
if (fields && fields.length > 0) {
|
||||
this.$refs.input[0].focus();
|
||||
this.$refs.input[0].select();
|
||||
} else if (otherInput && otherInput.length > 0) {
|
||||
otherInput[0].focus();
|
||||
otherInput[0].select();
|
||||
} else if (button) {
|
||||
button.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,106 @@
|
|||
var YlaTooltip = function () {
|
||||
// Private method
|
||||
function _createTooltip() {
|
||||
var tooltip = document.createElement('span');
|
||||
tooltip.className = 'yla-tooltip';
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
function _showTooltip(target, tooltip, placement) {
|
||||
// Put tooltip in document
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
// Calculate position for tooltip
|
||||
var placement = placement ? placement + '' : '',
|
||||
targetRect = target.getBoundingClientRect(),
|
||||
tooltipRect = tooltip.getBoundingClientRect(),
|
||||
targetCenterX = targetRect.left + (targetRect.width / 2),
|
||||
targetCenterY = targetRect.top + (targetRect.height / 2),
|
||||
className, tooltipX, tooltipY;
|
||||
|
||||
switch (placement.toLowerCase()) {
|
||||
case 'top':
|
||||
className = 'top';
|
||||
tooltipX = targetCenterX - (tooltipRect.width / 2);
|
||||
tooltipY = targetRect.top - tooltipRect.height;
|
||||
break;
|
||||
case 'bottom':
|
||||
className = 'bottom';
|
||||
tooltipX = targetCenterX - (tooltipRect.width / 2);
|
||||
tooltipY = targetRect.bottom;
|
||||
break;
|
||||
case 'left':
|
||||
className = 'left';
|
||||
tooltipX = targetRect.left - tooltipRect.width;
|
||||
tooltipY = targetCenterY - (tooltipRect.height / 2);
|
||||
break;
|
||||
case 'right':
|
||||
default:
|
||||
className = 'right';
|
||||
tooltipX = targetRect.right;
|
||||
tooltipY = targetCenterY - (tooltipRect.height / 2);
|
||||
break;
|
||||
}
|
||||
|
||||
// Position tooltip
|
||||
tooltip.style.position = 'fixed';
|
||||
tooltip.style.top = tooltipY + 'px';
|
||||
tooltip.style.left = tooltipX + 'px';
|
||||
tooltip.className = 'yla-tooltip ' + className;
|
||||
}
|
||||
|
||||
function _removeTooltip(tooltip) {
|
||||
document.body.removeChild(tooltip);
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
tooltip: _createTooltip()
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
content: {
|
||||
immediate: true,
|
||||
handler: function () {
|
||||
this.tooltip.textContent = this.content;
|
||||
}
|
||||
}
|
||||
},
|
||||
render: function (createElement) {
|
||||
// Make sure this component contain at least one element
|
||||
var nodes = this.$slots.default || [],
|
||||
mainElement = nodes.find(node => {
|
||||
return node.tag && node.tag !== '';
|
||||
});
|
||||
|
||||
if (!mainElement) return;
|
||||
|
||||
// Set event handler for main element
|
||||
var newData = mainElement.data || {};
|
||||
|
||||
newData.on = newData.on || {};
|
||||
newData.on.mouseenter = (evt) => {
|
||||
_showTooltip(evt.target, this.tooltip, this.placement);
|
||||
};
|
||||
|
||||
newData.on.mouseleave = () => {
|
||||
_removeTooltip(this.tooltip);
|
||||
};
|
||||
|
||||
// Return main element
|
||||
mainElement.data = newData;
|
||||
return mainElement;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
var Base = function () {
|
||||
return {
|
||||
data() {
|
||||
return {
|
||||
dialog: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_defaultDialog() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
title: '',
|
||||
content: '',
|
||||
fields: [],
|
||||
showLabel: false,
|
||||
mainText: 'Yes',
|
||||
secondText: '',
|
||||
mainClick: () => {
|
||||
this.dialog.visible = false;
|
||||
},
|
||||
secondClick: () => {
|
||||
this.dialog.visible = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
showDialog(cfg) {
|
||||
var base = this._defaultDialog();
|
||||
base.visible = true;
|
||||
if (cfg.loading) base.loading = cfg.loading;
|
||||
if (cfg.title) base.title = cfg.title;
|
||||
if (cfg.content) base.content = cfg.content;
|
||||
if (cfg.fields) base.fields = cfg.fields;
|
||||
if (cfg.showLabel) base.showLabel = cfg.showLabel;
|
||||
if (cfg.mainText) base.mainText = cfg.mainText;
|
||||
if (cfg.secondText) base.secondText = cfg.secondText;
|
||||
if (cfg.mainClick) base.mainClick = cfg.mainClick;
|
||||
if (cfg.secondClick) base.secondClick = cfg.secondClick;
|
||||
this.dialog = base;
|
||||
},
|
||||
showErrorDialog(msg) {
|
||||
this.showDialog({
|
||||
visible: true,
|
||||
title: 'Error',
|
||||
content: msg,
|
||||
mainText: 'OK',
|
||||
mainClick: () => {
|
||||
this.dialog.visible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,257 +0,0 @@
|
|||
! function (e, t) {
|
||||
"function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? module.exports = t() : e.salvattore = t()
|
||||
}(this, function () {
|
||||
window.matchMedia || (window.matchMedia = function () {
|
||||
"use strict";
|
||||
var e = window.styleMedia || window.media;
|
||||
if (!e) {
|
||||
var t = document.createElement("style"),
|
||||
n = document.getElementsByTagName("script")[0],
|
||||
r = null;
|
||||
t.type = "text/css", t.id = "matchmediajs-test", n.parentNode.insertBefore(t, n), r = "getComputedStyle" in window && window.getComputedStyle(t, null) || t.currentStyle, e = {
|
||||
matchMedium: function (e) {
|
||||
var n = "@media " + e + "{ #matchmediajs-test { width: 1px; } }";
|
||||
return t.styleSheet ? t.styleSheet.cssText = n : t.textContent = n, "1px" === r.width
|
||||
}
|
||||
}
|
||||
}
|
||||
return function (t) {
|
||||
return {
|
||||
matches: e.matchMedium(t || "all"),
|
||||
media: t || "all"
|
||||
}
|
||||
}
|
||||
}()),
|
||||
function () {
|
||||
"use strict";
|
||||
if (window.matchMedia && window.matchMedia("all").addListener) return !1;
|
||||
var e = window.matchMedia,
|
||||
t = e("only all").matches,
|
||||
n = !1,
|
||||
r = 0,
|
||||
a = [],
|
||||
i = function (t) {
|
||||
clearTimeout(r), r = setTimeout(function () {
|
||||
for (var t = 0, n = a.length; n > t; t++) {
|
||||
var r = a[t].mql,
|
||||
i = a[t].listeners || [],
|
||||
o = e(r.media).matches;
|
||||
if (o !== r.matches) {
|
||||
r.matches = o;
|
||||
for (var c = 0, l = i.length; l > c; c++) i[c].call(window, r)
|
||||
}
|
||||
}
|
||||
}, 30)
|
||||
};
|
||||
window.matchMedia = function (r) {
|
||||
var o = e(r),
|
||||
c = [],
|
||||
l = 0;
|
||||
return o.addListener = function (e) {
|
||||
t && (n || (n = !0, window.addEventListener("resize", i, !0)), 0 === l && (l = a.push({
|
||||
mql: o,
|
||||
listeners: c
|
||||
})), c.push(e))
|
||||
}, o.removeListener = function (e) {
|
||||
for (var t = 0, n = c.length; n > t; t++) c[t] === e && c.splice(t, 1)
|
||||
}, o
|
||||
}
|
||||
}(),
|
||||
function () {
|
||||
"use strict";
|
||||
for (var e = 0, t = ["ms", "moz", "webkit", "o"], n = 0; n < t.length && !window.requestAnimationFrame; ++n) window.requestAnimationFrame = window[t[n] + "RequestAnimationFrame"], window.cancelAnimationFrame = window[t[n] + "CancelAnimationFrame"] || window[t[n] + "CancelRequestAnimationFrame"];
|
||||
window.requestAnimationFrame || (window.requestAnimationFrame = function (t, n) {
|
||||
var r = (new Date).getTime(),
|
||||
a = Math.max(0, 16 - (r - e)),
|
||||
i = window.setTimeout(function () {
|
||||
t(r + a)
|
||||
}, a);
|
||||
return e = r + a, i
|
||||
}), window.cancelAnimationFrame || (window.cancelAnimationFrame = function (e) {
|
||||
clearTimeout(e)
|
||||
})
|
||||
}(), "function" != typeof window.CustomEvent && ! function () {
|
||||
"use strict";
|
||||
|
||||
function e(e, t) {
|
||||
t = t || {
|
||||
bubbles: !1,
|
||||
cancelable: !1,
|
||||
detail: void 0
|
||||
};
|
||||
var n = document.createEvent("CustomEvent");
|
||||
return n.initCustomEvent(e, t.bubbles, t.cancelable, t.detail), n
|
||||
}
|
||||
e.prototype = window.Event.prototype, window.CustomEvent = e
|
||||
}();
|
||||
var e = function (e, t, n) {
|
||||
"use strict";
|
||||
var r = {},
|
||||
a = [],
|
||||
i = [],
|
||||
o = [],
|
||||
c = function (e, t, n) {
|
||||
e.dataset ? e.dataset[t] = n : e.setAttribute("data-" + t, n)
|
||||
};
|
||||
return r.obtainGridSettings = function (t) {
|
||||
var n = e.getComputedStyle(t, ":before"),
|
||||
r = n.getPropertyValue("content").slice(1, -1),
|
||||
a = r.match(/^\s*(\d+)(?:\s?\.(.+))?\s*$/),
|
||||
i = 1,
|
||||
o = [];
|
||||
return a ? (i = a[1], o = a[2], o = o ? o.split(".") : ["column"]) : (a = r.match(/^\s*\.(.+)\s+(\d+)\s*$/), a && (o = a[1], i = a[2], i && (i = i.split(".")))), {
|
||||
numberOfColumns: i,
|
||||
columnClasses: o
|
||||
}
|
||||
}, r.addColumns = function (e, n) {
|
||||
for (var a, i = r.obtainGridSettings(e), o = i.numberOfColumns, l = i.columnClasses, s = new Array(+o), u = t.createDocumentFragment(), d = o; 0 !== d--;) a = "[data-columns] > *:nth-child(" + o + "n-" + d + ")", s.push(n.querySelectorAll(a));
|
||||
s.forEach(function (e) {
|
||||
var n = t.createElement("div"),
|
||||
r = t.createDocumentFragment();
|
||||
n.className = l.join(" "), Array.prototype.forEach.call(e, function (e) {
|
||||
r.appendChild(e)
|
||||
}), n.appendChild(r), u.appendChild(n)
|
||||
}), e.appendChild(u), c(e, "columns", o)
|
||||
}, r.removeColumns = function (n) {
|
||||
var r = t.createRange();
|
||||
r.selectNodeContents(n);
|
||||
var a = Array.prototype.filter.call(r.extractContents().childNodes, function (t) {
|
||||
return t instanceof e.HTMLElement
|
||||
}),
|
||||
i = a.length,
|
||||
o = a[0].childNodes.length,
|
||||
l = new Array(o * i);
|
||||
Array.prototype.forEach.call(a, function (e, t) {
|
||||
Array.prototype.forEach.call(e.children, function (e, n) {
|
||||
l[n * i + t] = e
|
||||
})
|
||||
});
|
||||
var s = t.createElement("div");
|
||||
return c(s, "columns", 0), l.filter(function (e) {
|
||||
return !!e
|
||||
}).forEach(function (e) {
|
||||
s.appendChild(e)
|
||||
}), s
|
||||
}, r.recreateColumns = function (t) {
|
||||
e.requestAnimationFrame(function () {
|
||||
r.addColumns(t, r.removeColumns(t));
|
||||
var e = new CustomEvent("columnsChange");
|
||||
t.dispatchEvent(e)
|
||||
})
|
||||
}, r.mediaQueryChange = function (e) {
|
||||
e.matches && Array.prototype.forEach.call(a, r.recreateColumns)
|
||||
}, r.getCSSRules = function (e) {
|
||||
var t;
|
||||
try {
|
||||
t = e.sheet.cssRules || e.sheet.rules
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
return t || []
|
||||
}, r.getStylesheets = function () {
|
||||
var e = Array.prototype.slice.call(t.querySelectorAll("style"));
|
||||
return e.forEach(function (t, n) {
|
||||
"text/css" !== t.type && "" !== t.type && e.splice(n, 1)
|
||||
}), Array.prototype.concat.call(e, Array.prototype.slice.call(t.querySelectorAll("link[rel='stylesheet']")))
|
||||
}, r.mediaRuleHasColumnsSelector = function (e) {
|
||||
var t, n;
|
||||
try {
|
||||
t = e.length
|
||||
} catch (e) {
|
||||
t = 0
|
||||
}
|
||||
for (; t--;)
|
||||
if (n = e[t], n.selectorText && n.selectorText.match(/\[data-columns\](.*)::?before$/)) return !0;
|
||||
return !1
|
||||
}, r.scanMediaQueries = function () {
|
||||
var t = [];
|
||||
if (e.matchMedia) {
|
||||
r.getStylesheets().forEach(function (e) {
|
||||
Array.prototype.forEach.call(r.getCSSRules(e), function (e) {
|
||||
try {
|
||||
e.media && e.cssRules && r.mediaRuleHasColumnsSelector(e.cssRules) && t.push(e)
|
||||
} catch (e) {}
|
||||
})
|
||||
});
|
||||
var n = i.filter(function (e) {
|
||||
return -1 === t.indexOf(e)
|
||||
});
|
||||
o.filter(function (e) {
|
||||
return -1 !== n.indexOf(e.rule)
|
||||
}).forEach(function (e) {
|
||||
e.mql.removeListener(r.mediaQueryChange)
|
||||
}), o = o.filter(function (e) {
|
||||
return -1 === n.indexOf(e.rule)
|
||||
}), t.filter(function (e) {
|
||||
return -1 == i.indexOf(e)
|
||||
}).forEach(function (t) {
|
||||
var n = e.matchMedia(t.media.mediaText);
|
||||
n.addListener(r.mediaQueryChange), o.push({
|
||||
rule: t,
|
||||
mql: n
|
||||
})
|
||||
}), i.length = 0, i = t
|
||||
}
|
||||
}, r.rescanMediaQueries = function () {
|
||||
r.scanMediaQueries(), Array.prototype.forEach.call(a, r.recreateColumns)
|
||||
}, r.nextElementColumnIndex = function (e, t) {
|
||||
var n, r, a, i = e.children,
|
||||
o = i.length,
|
||||
c = 0,
|
||||
l = 0;
|
||||
for (a = 0; o > a; a++) n = i[a], r = n.children.length + (t[a].children || t[a].childNodes).length, 0 === c && (c = r), c > r && (l = a, c = r);
|
||||
return l
|
||||
}, r.createFragmentsList = function (e) {
|
||||
for (var n = new Array(e), r = 0; r !== e;) n[r] = t.createDocumentFragment(), r++;
|
||||
return n
|
||||
}, r.appendElements = function (e, t) {
|
||||
var n = e.children,
|
||||
a = n.length,
|
||||
i = r.createFragmentsList(a);
|
||||
Array.prototype.forEach.call(t, function (t) {
|
||||
var n = r.nextElementColumnIndex(e, i);
|
||||
i[n].appendChild(t)
|
||||
}), Array.prototype.forEach.call(n, function (e, t) {
|
||||
e.appendChild(i[t])
|
||||
})
|
||||
}, r.prependElements = function (e, n) {
|
||||
var a = e.children,
|
||||
i = a.length,
|
||||
o = r.createFragmentsList(i),
|
||||
c = i - 1;
|
||||
n.forEach(function (e) {
|
||||
var t = o[c];
|
||||
t.insertBefore(e, t.firstChild), 0 === c ? c = i - 1 : c--
|
||||
}), Array.prototype.forEach.call(a, function (e, t) {
|
||||
e.insertBefore(o[t], e.firstChild)
|
||||
});
|
||||
for (var l = t.createDocumentFragment(), s = n.length % i; 0 !== s--;) l.appendChild(e.lastChild);
|
||||
e.insertBefore(l, e.firstChild)
|
||||
}, r.registerGrid = function (n) {
|
||||
if ("none" !== e.getComputedStyle(n).display) {
|
||||
var i = t.createRange();
|
||||
i.selectNodeContents(n);
|
||||
var o = t.createElement("div");
|
||||
o.appendChild(i.extractContents()), c(o, "columns", 0), r.addColumns(n, o), a.push(n)
|
||||
}
|
||||
}, r.init = function () {
|
||||
var e = t.createElement("style");
|
||||
e.innerHTML = "[data-columns]::before{display:block;visibility:hidden;position:absolute;font-size:1px;}", t.head.appendChild(e);
|
||||
var n = t.querySelectorAll("[data-columns]");
|
||||
Array.prototype.forEach.call(n, r.registerGrid), r.scanMediaQueries()
|
||||
}, r.init(), {
|
||||
appendElements: r.appendElements,
|
||||
prependElements: r.prependElements,
|
||||
registerGrid: r.registerGrid,
|
||||
recreateColumns: r.recreateColumns,
|
||||
rescanMediaQueries: r.rescanMediaQueries,
|
||||
init: r.init,
|
||||
append_elements: r.appendElements,
|
||||
prepend_elements: r.prependElements,
|
||||
register_grid: r.registerGrid,
|
||||
recreate_columns: r.recreateColumns,
|
||||
rescan_media_queries: r.rescanMediaQueries
|
||||
}
|
||||
}(window, window.document);
|
||||
return e
|
||||
});
|
|
@ -9,6 +9,10 @@
|
|||
hyphens: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1 0;
|
||||
}
|
||||
|
@ -22,7 +26,7 @@
|
|||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: @appBg;
|
||||
background-color: @bg;
|
||||
justify-content: center;
|
||||
>.error-message {
|
||||
width: 100%;
|
||||
|
@ -68,13 +72,13 @@
|
|||
align-items: baseline;
|
||||
padding: 8px;
|
||||
p {
|
||||
color: @fontLightColor;
|
||||
color: @colorLight;
|
||||
font-size: 0.9em;
|
||||
margin-right: 16px;
|
||||
min-width: 65px;
|
||||
}
|
||||
input {
|
||||
color: @fontColor;
|
||||
color: @color;
|
||||
padding: 8px;
|
||||
border: 1px solid @border;
|
||||
flex: 1 0;
|
||||
|
@ -83,13 +87,13 @@
|
|||
a {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
color: @fontLightColor;
|
||||
color: @colorLight;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
flex: 1 0;
|
||||
i {
|
||||
margin-right: 8px;
|
||||
color: @fontLightColor;
|
||||
color: @colorLight;
|
||||
}
|
||||
&:hover {
|
||||
color: @main;
|
||||
|
@ -102,9 +106,9 @@
|
|||
flex-flow: row nowrap;
|
||||
padding: 16px;
|
||||
a {
|
||||
color: @linkColor;
|
||||
color: @colorLink;
|
||||
text-transform: uppercase;
|
||||
background-color: @headerInputBg;
|
||||
background-color: @contentBg;
|
||||
flex: 1 0;
|
||||
text-align: center;
|
||||
&.button {
|
||||
|
@ -118,232 +122,174 @@
|
|||
}
|
||||
}
|
||||
|
||||
#main-page {
|
||||
background-color: @appBg;
|
||||
#index-page {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
#header {
|
||||
background-color: @contentBg;
|
||||
box-shadow: 0 0 3px @headerShadow;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
flex-flow: row nowrap;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: @bg;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
#sidebar {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
#n-selected {
|
||||
line-height: @headerHeight;
|
||||
font-size: 1.3em;
|
||||
color: @fontLightColor;
|
||||
flex: 1 0;
|
||||
border-right: 1px solid @border;
|
||||
padding: 0 32px;
|
||||
}
|
||||
flex-flow: column nowrap;
|
||||
flex-shrink: 0;
|
||||
background-color: @darkBg;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
#logo {
|
||||
border-left: 1px solid @border;
|
||||
cursor: default;
|
||||
flex-shrink: 0;
|
||||
.header-link;
|
||||
line-height: @headerHeight;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
font-size: 1.5em;
|
||||
font-weight: 100;
|
||||
color: @main;
|
||||
span {
|
||||
margin-right: 8px;
|
||||
}
|
||||
width: 60px;
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
color: @contentBg;
|
||||
background-color: @main;
|
||||
}
|
||||
>a {
|
||||
width: 60px;
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
color: @colorLight;
|
||||
&:hover {
|
||||
background-color: @appBg;
|
||||
background-color: @color;
|
||||
}
|
||||
}
|
||||
#search-box {
|
||||
align-items: center;
|
||||
border-right: 1px solid @border;
|
||||
display: flex;
|
||||
flex: 1 0;
|
||||
flex-flow: row nowrap;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
.button,
|
||||
input {
|
||||
background-color: @headerInputBg;
|
||||
border: 1px solid @border;
|
||||
color: @fontColor;
|
||||
font-size: 0.9em;
|
||||
padding: 8px;
|
||||
}
|
||||
.button {
|
||||
cursor: pointer;
|
||||
color: @linkColor;
|
||||
&:hover {
|
||||
color: @accent;
|
||||
}
|
||||
}
|
||||
input {
|
||||
border-right: 0;
|
||||
flex: 1 0;
|
||||
padding: 8px 16px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
#header-menu {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
a {
|
||||
line-height: @headerHeight;
|
||||
padding: 0 16px;
|
||||
color: @linkColor;
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid @border;
|
||||
}
|
||||
span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
&:hover {
|
||||
color: @main;
|
||||
background-color: @appBg;
|
||||
}
|
||||
&.active {
|
||||
background-color: @color;
|
||||
color: @main;
|
||||
}
|
||||
}
|
||||
}
|
||||
#main {
|
||||
margin-top: @headerHeight;
|
||||
#body {
|
||||
flex: 1 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
#input-bookmark {
|
||||
align-self: center;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
#header {
|
||||
background-color: @contentBg;
|
||||
border-bottom: 1px solid @border;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 32px 16px 20px;
|
||||
background-color: @headerInputBg;
|
||||
outline: 1px solid @border;
|
||||
>p {
|
||||
color: @fontColor;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
padding: 16px;
|
||||
&.error-message {
|
||||
color: @main;
|
||||
font-size: 0.9em;
|
||||
border-bottom: 1px solid @border;
|
||||
font-weight: 500;
|
||||
text-transform: none;
|
||||
}
|
||||
flex-flow: row nowrap;
|
||||
color: @color;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
input {
|
||||
flex: 1 0;
|
||||
line-height: 60px;
|
||||
padding: 0 16px;
|
||||
font-size: 1em;
|
||||
border-right: 1px solid @border;
|
||||
}
|
||||
input[type=text],
|
||||
textarea {
|
||||
outline: 1px solid @border;
|
||||
color: @fontColor;
|
||||
font-size: 0.9em;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 4em;
|
||||
max-height: 10em;
|
||||
}
|
||||
.button-area {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 8px;
|
||||
a {
|
||||
color: @linkColor;
|
||||
text-transform: uppercase;
|
||||
padding: 8px;
|
||||
background-color: @headerInputBg;
|
||||
font-size: 0.9em;
|
||||
&.button {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: @accent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#search-parameter {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: 0 8px;
|
||||
a {
|
||||
display: block;
|
||||
margin: 8px;
|
||||
padding: 8px;
|
||||
font-size: 0.9em;
|
||||
background-color: @fontLightColor;
|
||||
color: white;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: @main;
|
||||
text-decoration: line-through;
|
||||
width: 60px;
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
color: @colorLink;
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: @main;
|
||||
}
|
||||
}
|
||||
}
|
||||
#grid {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 4px;
|
||||
>.column {
|
||||
flex: 1 0;
|
||||
padding: 12px;
|
||||
max-width: 100%;
|
||||
>*:not(:last-child) {
|
||||
margin-bottom: 24px;
|
||||
overflow-y: auto;
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-gap: 16px;
|
||||
padding: 16px;
|
||||
.bookmark {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-width: 0;
|
||||
border: 1px solid @border;
|
||||
background-color: @contentBg;
|
||||
height: 100%;
|
||||
&:last-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
&:hover {
|
||||
.bookmark-menu>a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.bookmark-content {
|
||||
display: block;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
&:hover,
|
||||
&:focus {
|
||||
.title {
|
||||
color: @main;
|
||||
}
|
||||
}
|
||||
>*:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
padding: 0 16px;
|
||||
color: @color;
|
||||
&:first-child {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
.excerpt {
|
||||
color: @color;
|
||||
padding: 0 16px;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.5em;
|
||||
max-height: 12em;
|
||||
}
|
||||
}
|
||||
.bookmark-menu {
|
||||
padding: 8px 16px 16px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
align-items: center;
|
||||
a {
|
||||
color: @colorLink;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.8;
|
||||
display: none;
|
||||
font-size: 0.9em;
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: @main;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.url {
|
||||
flex: 1 0;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#message-bar {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
padding: 32px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
margin-top: -60px;
|
||||
height: 120px;
|
||||
i {
|
||||
color: @fontLightColor;
|
||||
font-size: 3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
#header {
|
||||
position: static;
|
||||
#header-menu>a>span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#main {
|
||||
margin-top: 0;
|
||||
.bookmark-menu>a>span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 740px) {
|
||||
#main #input-bookmark {
|
||||
width: auto;
|
||||
align-self: auto;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 500px) {
|
||||
#header #logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -375,7 +321,7 @@
|
|||
flex: 1 0;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
color: @linkColor;
|
||||
color: @colorLink;
|
||||
padding: 16px;
|
||||
i {
|
||||
margin-right: 4px;
|
||||
|
@ -385,7 +331,7 @@
|
|||
border-right: 1px solid @border;
|
||||
}
|
||||
&:visited {
|
||||
color: @linkColor
|
||||
color: @colorLink
|
||||
}
|
||||
&:hover {
|
||||
color: @main;
|
||||
|
@ -406,7 +352,7 @@
|
|||
}
|
||||
p {
|
||||
font-size: 0.9em;
|
||||
color: @fontColor;
|
||||
color: @color;
|
||||
}
|
||||
}
|
||||
#content {
|
||||
|
@ -438,263 +384,4 @@
|
|||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#dialog-overlay {
|
||||
.full-overlay();
|
||||
#dialog {
|
||||
display: flex;
|
||||
background-color: @contentBg;
|
||||
align-self: center;
|
||||
flex-flow: column;
|
||||
border: 1px solid @border;
|
||||
max-width: 500px;
|
||||
#dialog-title {
|
||||
color: @fontColor;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
padding: 16px;
|
||||
font-size: 1em;
|
||||
border-bottom: 1px solid @border;
|
||||
}
|
||||
#dialog-content {
|
||||
padding: 16px;
|
||||
}
|
||||
#dialog-button {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 8px;
|
||||
border-top: 1px solid @border;
|
||||
a {
|
||||
color: @linkColor;
|
||||
text-transform: uppercase;
|
||||
padding: 8px;
|
||||
background-color: @headerInputBg;
|
||||
&.button {
|
||||
cursor: pointer;
|
||||
&:not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
&:hover {
|
||||
color: @accent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#tag-cloud-overlay {
|
||||
.full-overlay();
|
||||
#tag-cloud {
|
||||
display: flex;
|
||||
background-color: @contentBg;
|
||||
flex-flow: column nowrap;
|
||||
border: 1px solid @border;
|
||||
max-height: 100%;
|
||||
#tag-cloud-title {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid @border;
|
||||
align-items: center;
|
||||
p {
|
||||
color: @fontColor;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 1em;
|
||||
flex: 1 0;
|
||||
}
|
||||
a {
|
||||
color: @fontLightColor;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: @main;
|
||||
}
|
||||
}
|
||||
}
|
||||
#tag-cloud-content {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
a {
|
||||
color: @fontLightColor;
|
||||
cursor: pointer;
|
||||
margin: 4px;
|
||||
&:hover {
|
||||
color: @main;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: @main !important;
|
||||
font-size: 0.9em;
|
||||
&::before {
|
||||
content: "\f071";
|
||||
font-weight: 900;
|
||||
margin-right: 8px;
|
||||
font-family: "Font Awesome 5 Free"
|
||||
}
|
||||
}
|
||||
|
||||
.bookmark {
|
||||
background-color: @contentBg;
|
||||
border: 1px solid @border;
|
||||
position: relative;
|
||||
.checkbox {
|
||||
z-index: 9;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
outline: 1px solid @border;
|
||||
color: @linkColor;
|
||||
background-color: @contentBg;
|
||||
width: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
&:hover {
|
||||
color: @accent !important;
|
||||
}
|
||||
}
|
||||
.bookmark-metadata {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
border-bottom: 1px solid @border;
|
||||
.bookmark-time {
|
||||
color: @fontLightColor;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.bookmark-title {
|
||||
color: @fontColor;
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bookmark-url {
|
||||
.bookmark-time;
|
||||
margin-bottom: 0;
|
||||
margin-top: 8px;
|
||||
max-height: 2.6em;
|
||||
line-height: 1.3em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
&.has-image {
|
||||
min-height: 250px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
&::before {
|
||||
content: "";
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
.bookmark-time,
|
||||
.bookmark-url {
|
||||
z-index: 2;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.bookmark-title {
|
||||
z-index: 2;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.bookmark-title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bookmark-excerpt {
|
||||
padding: 16px 16px 0;
|
||||
color: @fontColor;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bookmark-tags {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: 12px 12px 0;
|
||||
margin-bottom: -4px;
|
||||
a {
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
padding: 4px;
|
||||
color: @main !important;
|
||||
&::before {
|
||||
content: "#"
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bookmark-menu {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
border-top: 1px solid @border;
|
||||
visibility: hidden;
|
||||
margin-top: 16px;
|
||||
&:nth-child(3) {
|
||||
border-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
a {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
flex: 1 0;
|
||||
color: @linkColor !important;
|
||||
padding: 8px;
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
span {
|
||||
margin-left: 4px;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid @border;
|
||||
}
|
||||
&:hover {
|
||||
color: @accent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
.bookmark-menu {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
&.checked {
|
||||
border: 1px solid @borderDark;
|
||||
outline: 6px solid @borderDark;
|
||||
.checkbox {
|
||||
opacity: 1;
|
||||
outline: 0;
|
||||
background-color: @borderDark;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +1,36 @@
|
|||
// out: false
|
||||
@appBg: #F5F5F5;
|
||||
//
|
||||
// Background
|
||||
@bg: #EEE;
|
||||
@darkBg: #353535;
|
||||
@contentBg: #FFF;
|
||||
//
|
||||
// Border
|
||||
@border: #E5E5E5;
|
||||
@borderDark: #9E9E9E;
|
||||
@contentBg: #FFF;
|
||||
@headerInputBg: #FFF;
|
||||
@fontColor: #000;
|
||||
@linkColor: #535A60;
|
||||
@fontLightColor: #6F757A;
|
||||
//
|
||||
// Font color
|
||||
@color: #232323;
|
||||
@colorLink: #999;
|
||||
@colorLight: #EEE;
|
||||
@fontSize: 0.9em;
|
||||
@headerHeight: 70px;
|
||||
//
|
||||
// Color theme
|
||||
@main: #F44336;
|
||||
@accent: #F44336;
|
||||
@headerShadow: rgba(0, 0, 0, 0.3);
|
||||
@mainDark: #B71C1C;
|
||||
@accent: #f4a236;
|
||||
//
|
||||
// Tooltip
|
||||
@tooltipBg: #232323;
|
||||
@arrowWidth: 8px;
|
||||
//
|
||||
// Other variable
|
||||
@headerHeight: 60px;
|
||||
//
|
||||
// Mixin
|
||||
.header-link {
|
||||
border-right: 1px solid @border;
|
||||
color: @fontColor;
|
||||
color: @color;
|
||||
cursor: pointer;
|
||||
font-size: @fontSize;
|
||||
line-height: @headerHeight;
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
@import "variable";
|
||||
.yla-dialog__overlay {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 10001;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
padding: 32px;
|
||||
.yla-dialog {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-width: 400px;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #FFF;
|
||||
font-size: 16px;
|
||||
>.yla-dialog__header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 16px;
|
||||
min-height: 0;
|
||||
background-color: @darkBg;
|
||||
color: @colorLight;
|
||||
flex-shrink: 0;
|
||||
>p {
|
||||
flex: 1 0;
|
||||
font-weight: 600;
|
||||
font-size: 1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
>a:hover {
|
||||
color: @main;
|
||||
}
|
||||
}
|
||||
>.yla-dialog__body {
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
max-height: 100%;
|
||||
min-height: 80px;
|
||||
min-width: 0;
|
||||
font-size: 0.9em;
|
||||
overflow: auto;
|
||||
grid-template-columns: max-content 1fr;
|
||||
align-items: baseline;
|
||||
grid-gap: 16px;
|
||||
>.yla-dialog__content {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
align-self: baseline;
|
||||
}
|
||||
>input {
|
||||
color: @color;
|
||||
padding: 8px;
|
||||
border: 1px solid @border;
|
||||
}
|
||||
>.suggestion {
|
||||
position: absolute;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
background-color: @bg;
|
||||
border: 1px solid @border;
|
||||
}
|
||||
}
|
||||
>.yla-dialog__footer {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid @border;
|
||||
>a {
|
||||
text-transform: uppercase;
|
||||
padding: 0 8px;
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
&:hover {
|
||||
color: @main;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
color: @main;
|
||||
border-bottom: 1px dashed @main;
|
||||
}
|
||||
}
|
||||
>i {
|
||||
width: 19px;
|
||||
line-height: 19px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
@import "variable";
|
||||
.yla-tooltip {
|
||||
font-size: 14px;
|
||||
color: @colorLight;
|
||||
background-color: @tooltipBg;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
border: @arrowWidth solid transparent;
|
||||
}
|
||||
&.left {
|
||||
margin-left: -(@arrowWidth*2-2);
|
||||
&::after {
|
||||
top: 50%;
|
||||
right: -2*@arrowWidth;
|
||||
margin-top: -@arrowWidth;
|
||||
border-left-color: @tooltipBg
|
||||
}
|
||||
}
|
||||
&.top {
|
||||
margin-top: -(@arrowWidth*2-2);
|
||||
&::after {
|
||||
left: 50%;
|
||||
bottom: -2*@arrowWidth;
|
||||
margin-left: -@arrowWidth;
|
||||
border-top-color: @tooltipBg
|
||||
}
|
||||
}
|
||||
&.right {
|
||||
margin-left: @arrowWidth*2-2;
|
||||
&::after {
|
||||
top: 50%;
|
||||
left: -2*@arrowWidth;
|
||||
margin-top: -@arrowWidth;
|
||||
border-right-color: @tooltipBg
|
||||
}
|
||||
}
|
||||
&.bottom {
|
||||
margin-top: @arrowWidth*2-2;
|
||||
&::after {
|
||||
left: 50%;
|
||||
top: -2*@arrowWidth;
|
||||
margin-left: -@arrowWidth;
|
||||
border-bottom-color: @tooltipBg
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue