mirror of
https://github.com/go-shiori/shiori.git
synced 2025-09-09 22:49:30 +08:00
753 lines
No EOL
27 KiB
JavaScript
753 lines
No EOL
27 KiB
JavaScript
var template = `
|
|
<div id="page-home">
|
|
<div class="page-header">
|
|
<input type="text" placeholder="Search url, keyword or tags" v-model.trim="search" @focus="$event.target.select()" @keyup.enter="searchBookmarks"/>
|
|
<a title="Refresh storage" @click="reloadData">
|
|
<i class="fas fa-fw fa-sync-alt" :class="loading && 'fa-spin'"></i>
|
|
</a>
|
|
<a title="Add new bookmark" @click="showDialogAdd">
|
|
<i class="fas fa-fw fa-plus-circle"></i>
|
|
</a>
|
|
<a title="Show tags" @click="showDialogTags">
|
|
<i class="fas fa-fw fa-tags"></i>
|
|
</a>
|
|
<a title="Batch edit" @click="toggleEditMode">
|
|
<i class="fas fa-fw fa-pencil-alt"></i>
|
|
</a>
|
|
</div>
|
|
<div class="page-header" id="edit-box" v-if="editMode">
|
|
<p>{{selection.length}} items selected</p>
|
|
<a title="Delete bookmark" @click="showDialogDelete(selection)">
|
|
<i class="fas fa-fw fa-trash-alt"></i>
|
|
</a>
|
|
<a title="Add tags" @click="showDialogAddTags(selection)">
|
|
<i class="fas fa-fw fa-tags"></i>
|
|
</a>
|
|
<a title="Update archives" @click="showDialogUpdateCache(selection)">
|
|
<i class="fas fa-fw fa-cloud-download-alt"></i>
|
|
</a>
|
|
<a title="Cancel" @click="toggleEditMode">
|
|
<i class="fas fa-fw fa-times"></i>
|
|
</a>
|
|
</div>
|
|
<div id="bookmarks-grid" ref="bookmarksGrid" :class="{list: displayOptions.listMode}">
|
|
<pagination-box v-if="maxPage > 1"
|
|
:page="page"
|
|
:maxPage="maxPage"
|
|
:editMode="editMode"
|
|
@change="changePage">
|
|
</pagination-box>
|
|
<bookmark-item v-for="(book, index) in bookmarks"
|
|
v-bind="book"
|
|
:index="index"
|
|
:key="book.id"
|
|
:editMode="editMode"
|
|
:showId="displayOptions.showId"
|
|
:listMode="displayOptions.listMode"
|
|
:selected="isSelected(book.id)"
|
|
@select="toggleSelection"
|
|
@tag-clicked="filterTag"
|
|
@edit="showDialogEdit"
|
|
@delete="showDialogDelete"
|
|
@update="showDialogUpdateCache">
|
|
</bookmark-item>
|
|
<pagination-box v-if="maxPage > 1"
|
|
:page="page"
|
|
:maxPage="maxPage"
|
|
:editMode="editMode"
|
|
@change="changePage">
|
|
</pagination-box>
|
|
</div>
|
|
<p class="empty-message" v-if="!loading && listIsEmpty">No saved bookmarks yet :(</p>
|
|
<div class="loading-overlay" v-if="loading"><i class="fas fa-fw fa-spin fa-spinner"></i></div>
|
|
<custom-dialog id="dialog-tags" v-bind="dialogTags">
|
|
<a v-for="(tag, idx) in tags" @click="tagClicked(idx, tag)">
|
|
{{tag.name}}<span>{{tag.nBookmarks}}</span>
|
|
</a>
|
|
</custom-dialog>
|
|
<custom-dialog v-bind="dialog"/>
|
|
</div>`
|
|
|
|
import paginationBox from "../component/pagination.js";
|
|
import bookmarkItem from "../component/bookmark.js";
|
|
import customDialog from "../component/dialog.js";
|
|
import basePage from "./base.js";
|
|
|
|
export default {
|
|
template: template,
|
|
mixins: [basePage],
|
|
components: {
|
|
bookmarkItem,
|
|
paginationBox,
|
|
customDialog
|
|
},
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
editMode: false,
|
|
selection: [],
|
|
|
|
search: "",
|
|
page: 0,
|
|
maxPage: 0,
|
|
bookmarks: [],
|
|
tags: [],
|
|
|
|
dialogTags: {
|
|
visible: false,
|
|
editMode: false,
|
|
title: 'Existing Tags',
|
|
mainText: 'OK',
|
|
secondText: 'Rename Tags',
|
|
mainClick: () => {
|
|
if (this.dialogTags.editMode) {
|
|
this.dialogTags.editMode = false;
|
|
} else {
|
|
this.dialogTags.visible = false;
|
|
}
|
|
},
|
|
secondClick: () => {
|
|
this.dialogTags.editMode = true;
|
|
},
|
|
escPressed: () => {
|
|
this.dialogTags.visible = false;
|
|
this.dialogTags.editMode = false;
|
|
}
|
|
},
|
|
}
|
|
},
|
|
computed: {
|
|
listIsEmpty() {
|
|
return this.bookmarks.length <= 0;
|
|
}
|
|
},
|
|
watch: {
|
|
"dialogTags.editMode"(editMode) {
|
|
if (editMode) {
|
|
this.dialogTags.title = "Rename Tags";
|
|
this.dialogTags.mainText = "Cancel";
|
|
this.dialogTags.secondText = "";
|
|
} else {
|
|
this.dialogTags.title = "Existing Tags";
|
|
this.dialogTags.mainText = "OK";
|
|
this.dialogTags.secondText = "Rename Tags";
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
reloadData() {
|
|
if (this.loading) return;
|
|
this.page = 1;
|
|
this.search = "";
|
|
this.loadData(true, true);
|
|
},
|
|
loadData(saveState, fetchTags) {
|
|
if (this.loading) return;
|
|
|
|
// Set default args
|
|
saveState = (typeof saveState === "boolean") ? saveState : true;
|
|
fetchTags = (typeof fetchTags === "boolean") ? fetchTags : false;
|
|
|
|
// Parse search query
|
|
var rxTagA = /['"]#([^'"]+)['"]/g, // "#tag with space"
|
|
rxTagB = /(^|\s+)#(\S+)/g, // #tag-without-space
|
|
keyword = this.search,
|
|
tags = [],
|
|
rxResult;
|
|
|
|
// Fetch tag A first
|
|
while (rxResult = rxTagA.exec(keyword)) {
|
|
tags.push(rxResult[1]);
|
|
}
|
|
|
|
// Clear tag A from keyword
|
|
keyword = keyword.replace(rxTagA, "");
|
|
|
|
// Fetch tag B
|
|
while (rxResult = rxTagB.exec(keyword)) {
|
|
tags.push(rxResult[2]);
|
|
}
|
|
|
|
// Clear tag B from keyword, then trim keyword
|
|
keyword = keyword.replace(rxTagB, "").trim().replace(/\s+/g, " ");
|
|
|
|
// Prepare URL for API
|
|
var url = new URL("/api/bookmarks", document.URL);
|
|
url.search = new URLSearchParams({
|
|
keyword: keyword,
|
|
tags: tags.join(","),
|
|
page: this.page
|
|
});
|
|
|
|
// Fetch data from API
|
|
var skipFetchTags = Error("skip fetching tags");
|
|
|
|
this.loading = true;
|
|
fetch(url)
|
|
.then(response => {
|
|
if (!response.ok) throw response;
|
|
return response.json();
|
|
})
|
|
.then(json => {
|
|
// Set data
|
|
this.page = json.page;
|
|
this.maxPage = json.maxPage;
|
|
this.bookmarks = json.bookmarks;
|
|
|
|
// Save state and change URL if needed
|
|
if (saveState) {
|
|
var history = {
|
|
activePage: "page-home",
|
|
search: this.search,
|
|
page: this.page
|
|
};
|
|
|
|
var urlQueries = [];
|
|
if (this.page > 1) urlQueries.push(`page=${this.page}`);
|
|
if (this.search !== "") urlQueries.push(`search=${this.search}`);
|
|
|
|
var url = "#home"
|
|
if (urlQueries.length > 0) {
|
|
url += `?${urlQueries.join("&")}`;
|
|
}
|
|
|
|
window.history.pushState(history, "page-home", url);
|
|
}
|
|
|
|
// Fetch tags if requested
|
|
if (fetchTags) {
|
|
return fetch("/api/tags");
|
|
} else {
|
|
this.loading = false;
|
|
throw skipFetchTags;
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) throw response;
|
|
return response.json();
|
|
})
|
|
.then(json => {
|
|
this.tags = json;
|
|
this.loading = false;
|
|
})
|
|
.catch(err => {
|
|
this.loading = false;
|
|
|
|
if (err !== skipFetchTags) {
|
|
err.text().then(msg => {
|
|
this.showErrorDialog(msg, err.status);
|
|
})
|
|
}
|
|
});
|
|
},
|
|
searchBookmarks() {
|
|
this.page = 1;
|
|
this.loadData();
|
|
},
|
|
changePage(page) {
|
|
this.page = page;
|
|
this.$refs.bookmarksGrid.scrollTop = 0;
|
|
this.loadData();
|
|
},
|
|
toggleEditMode() {
|
|
this.selection = [];
|
|
this.editMode = !this.editMode;
|
|
},
|
|
toggleSelection(item) {
|
|
var idx = this.selection.findIndex(el => el.id === item.id);
|
|
if (idx === -1) this.selection.push(item);
|
|
else this.selection.splice(idx, 1);
|
|
},
|
|
isSelected(bookId) {
|
|
return this.selection.findIndex(el => el.id === bookId) > -1;
|
|
},
|
|
tagClicked(idx, tag) {
|
|
if (!this.dialogTags.editMode) {
|
|
this.filterTag(tag.name);
|
|
} else {
|
|
this.dialogTags.visible = false;
|
|
this.showDialogRenameTag(idx, tag);
|
|
}
|
|
},
|
|
filterTag(tagName) {
|
|
var rxSpace = /\s+/g,
|
|
newTag = rxSpace.test(tagName) ? `"#${tagName}"` : `#${tagName}`;
|
|
|
|
if (!this.search.includes(newTag)) {
|
|
this.search += ` ${newTag}`;
|
|
this.loadData();
|
|
}
|
|
},
|
|
showDialogAdd() {
|
|
this.showDialog({
|
|
title: "New Bookmark",
|
|
content: "Create a new bookmark",
|
|
fields: [{
|
|
name: "url",
|
|
label: "Url, start with http://...",
|
|
}, {
|
|
name: "title",
|
|
label: "Custom title (optional)"
|
|
}, {
|
|
name: "excerpt",
|
|
label: "Custom excerpt (optional)",
|
|
type: "area"
|
|
}, {
|
|
name: "tags",
|
|
label: "Comma separated tags (optional)",
|
|
separator: ",",
|
|
dictionary: this.tags.map(tag => tag.name)
|
|
}, {
|
|
name: "createArchive",
|
|
label: "Create archive",
|
|
type: "check",
|
|
value: this.displayOptions.useArchive,
|
|
}],
|
|
mainText: "OK",
|
|
secondText: "Cancel",
|
|
mainClick: (data) => {
|
|
// Make sure URL is not empty
|
|
if (data.url.trim() === "") {
|
|
this.showErrorDialog("URL must not empty");
|
|
return;
|
|
}
|
|
|
|
// Prepare tags
|
|
var tags = data.tags
|
|
.toLowerCase()
|
|
.replace(/\s+/g, " ")
|
|
.split(/\s*,\s*/g)
|
|
.filter(tag => tag.trim() !== "")
|
|
.map(tag => {
|
|
return {
|
|
name: tag.trim()
|
|
};
|
|
});
|
|
|
|
// Send data
|
|
var data = {
|
|
url: data.url.trim(),
|
|
title: data.title.trim(),
|
|
excerpt: data.excerpt.trim(),
|
|
tags: tags,
|
|
createArchive: data.createArchive,
|
|
};
|
|
|
|
this.dialog.loading = true;
|
|
fetch("/api/bookmarks", {
|
|
method: "post",
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) throw response;
|
|
return response.json();
|
|
})
|
|
.then(json => {
|
|
this.dialog.loading = false;
|
|
this.dialog.visible = false;
|
|
this.bookmarks.splice(0, 0, json);
|
|
})
|
|
.catch(err => {
|
|
this.dialog.loading = false;
|
|
err.text().then(msg => {
|
|
this.showErrorDialog(msg, err.status);
|
|
})
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogEdit(item) {
|
|
// Check the item
|
|
if (typeof item !== "object") return;
|
|
|
|
var id = (typeof item.id === "number") ? item.id : 0,
|
|
index = (typeof item.index === "number") ? item.index : -1;
|
|
|
|
if (id < 1 || index < 0) return;
|
|
|
|
// Get the existing bookmark value
|
|
var book = JSON.parse(JSON.stringify(this.bookmarks[index])),
|
|
strTags = book.tags.map(tag => tag.name).join(", ");
|
|
|
|
this.showDialog({
|
|
title: "Edit Bookmark",
|
|
content: "Edit the bookmark's data",
|
|
showLabel: true,
|
|
fields: [{
|
|
name: "title",
|
|
label: "Title",
|
|
value: book.title,
|
|
}, {
|
|
name: "excerpt",
|
|
label: "Excerpt",
|
|
type: "area",
|
|
value: book.excerpt,
|
|
}, {
|
|
name: "tags",
|
|
label: "Tags",
|
|
value: strTags,
|
|
separator: ",",
|
|
dictionary: this.tags.map(tag => tag.name)
|
|
}],
|
|
mainText: "OK",
|
|
secondText: "Cancel",
|
|
mainClick: (data) => {
|
|
// Validate input
|
|
if (data.title.trim() === "") return;
|
|
|
|
// Prepare tags
|
|
var tags = data.tags
|
|
.toLowerCase()
|
|
.replace(/\s+/g, " ")
|
|
.split(/\s*,\s*/g)
|
|
.filter(tag => tag.trim() !== "")
|
|
.map(tag => {
|
|
return {
|
|
name: tag.trim()
|
|
};
|
|
});
|
|
|
|
// Set new data
|
|
book.title = data.title.trim();
|
|
book.excerpt = data.excerpt.trim();
|
|
book.tags = tags;
|
|
|
|
// Send data
|
|
this.dialog.loading = true;
|
|
fetch("/api/bookmarks", {
|
|
method: "put",
|
|
body: JSON.stringify(book),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) throw response;
|
|
return response.json();
|
|
})
|
|
.then(json => {
|
|
this.dialog.loading = false;
|
|
this.dialog.visible = false;
|
|
this.bookmarks.splice(index, 1, json);
|
|
})
|
|
.catch(err => {
|
|
this.dialog.loading = false;
|
|
err.text().then(msg => {
|
|
this.showErrorDialog(msg, err.status);
|
|
})
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogDelete(items) {
|
|
// Check and filter items
|
|
if (typeof items !== "object") return;
|
|
if (!Array.isArray(items)) items = [items];
|
|
|
|
items = items.filter(item => {
|
|
var id = (typeof item.id === "number") ? item.id : 0,
|
|
index = (typeof item.index === "number") ? item.index : -1;
|
|
|
|
return id > 0 && index > -1;
|
|
});
|
|
|
|
if (items.length === 0) return;
|
|
|
|
// Split ids and indices
|
|
var ids = items.map(item => item.id),
|
|
indices = items.map(item => item.index).sort((a, b) => b - a);
|
|
|
|
// Create title and content
|
|
var title = "Delete Bookmarks",
|
|
content = "Delete the selected bookmarks ? This action is irreversible.";
|
|
|
|
if (items.length === 1) {
|
|
title = "Delete Bookmark";
|
|
content = "Are you sure ? This action is irreversible.";
|
|
}
|
|
|
|
// Show dialog
|
|
this.showDialog({
|
|
title: title,
|
|
content: content,
|
|
mainText: "Yes",
|
|
secondText: "No",
|
|
mainClick: () => {
|
|
this.dialog.loading = true;
|
|
fetch("/api/bookmarks", {
|
|
method: "delete",
|
|
body: JSON.stringify(ids),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) throw response;
|
|
return response;
|
|
})
|
|
.then(() => {
|
|
this.selection = [];
|
|
this.editMode = false;
|
|
this.dialog.loading = false;
|
|
this.dialog.visible = false;
|
|
indices.forEach(index => this.bookmarks.splice(index, 1))
|
|
|
|
if (this.bookmarks.length < 20) {
|
|
this.loadData(false);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
this.selection = [];
|
|
this.editMode = false;
|
|
this.dialog.loading = false;
|
|
|
|
err.text().then(msg => {
|
|
this.showErrorDialog(msg, err.status);
|
|
})
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogUpdateCache(items) {
|
|
// Check and filter items
|
|
if (typeof items !== "object") return;
|
|
if (!Array.isArray(items)) items = [items];
|
|
|
|
items = items.filter(item => {
|
|
var id = (typeof item.id === "number") ? item.id : 0,
|
|
index = (typeof item.index === "number") ? item.index : -1;
|
|
|
|
return id > 0 && index > -1;
|
|
});
|
|
|
|
if (items.length === 0) return;
|
|
|
|
// Show dialog
|
|
var ids = items.map(item => item.id);
|
|
|
|
this.showDialog({
|
|
title: "Update Cache",
|
|
content: "Update cache for selected bookmarks ? This action is irreversible.",
|
|
fields: [{
|
|
name: "createArchive",
|
|
label: "Update archive as well",
|
|
type: "check",
|
|
value: this.displayOptions.useArchive,
|
|
}],
|
|
mainText: "Yes",
|
|
secondText: "No",
|
|
mainClick: (data) => {
|
|
var data = {
|
|
ids: ids,
|
|
createArchive: data.createArchive,
|
|
};
|
|
|
|
this.dialog.loading = true;
|
|
fetch("/api/cache", {
|
|
method: "put",
|
|
body: JSON.stringify(data),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) throw response;
|
|
return response.json();
|
|
})
|
|
.then(json => {
|
|
this.selection = [];
|
|
this.editMode = false;
|
|
this.dialog.loading = false;
|
|
this.dialog.visible = false;
|
|
|
|
json.forEach(book => {
|
|
var item = items.find(el => el.id === book.id);
|
|
this.bookmarks.splice(item.index, 1, book);
|
|
});
|
|
})
|
|
.catch(err => {
|
|
this.selection = [];
|
|
this.editMode = false;
|
|
this.dialog.loading = false;
|
|
|
|
err.text().then(msg => {
|
|
this.showErrorDialog(msg, err.status);
|
|
})
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogAddTags(items) {
|
|
// Check and filter items
|
|
if (typeof items !== "object") return;
|
|
if (!Array.isArray(items)) items = [items];
|
|
|
|
items = items.filter(item => {
|
|
var id = (typeof item.id === "number") ? item.id : 0,
|
|
index = (typeof item.index === "number") ? item.index : -1;
|
|
|
|
return id > 0 && index > -1;
|
|
});
|
|
|
|
if (items.length === 0) return;
|
|
|
|
// Show dialog
|
|
this.showDialog({
|
|
title: "Add New Tags",
|
|
content: "Add new tags to selected bookmarks",
|
|
fields: [{
|
|
name: "tags",
|
|
label: "Comma separated tags",
|
|
value: "",
|
|
separator: ",",
|
|
dictionary: this.tags.map(tag => tag.name)
|
|
}],
|
|
mainText: 'OK',
|
|
secondText: 'Cancel',
|
|
mainClick: (data) => {
|
|
// Validate input
|
|
var tags = data.tags
|
|
.toLowerCase()
|
|
.replace(/\s+/g, ' ')
|
|
.split(/\s*,\s*/g)
|
|
.filter(tag => tag.trim() !== '')
|
|
.map(tag => {
|
|
return {
|
|
name: tag.trim()
|
|
};
|
|
});
|
|
|
|
if (tags.length === 0) return;
|
|
|
|
// Send data
|
|
var request = {
|
|
ids: items.map(item => item.id),
|
|
tags: tags
|
|
}
|
|
|
|
this.dialog.loading = true;
|
|
fetch("/api/bookmarks/tags", {
|
|
method: "put",
|
|
body: JSON.stringify(request),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) throw response;
|
|
return response.json();
|
|
})
|
|
.then(json => {
|
|
this.selection = [];
|
|
this.editMode = false;
|
|
this.dialog.loading = false;
|
|
this.dialog.visible = false;
|
|
|
|
json.forEach(book => {
|
|
var item = items.find(el => el.id === book.id);
|
|
this.bookmarks.splice(item.index, 1, book);
|
|
});
|
|
})
|
|
.catch(err => {
|
|
this.selection = [];
|
|
this.editMode = false;
|
|
this.dialog.loading = false;
|
|
|
|
err.text().then(msg => {
|
|
this.showErrorDialog(msg, err.status);
|
|
})
|
|
});
|
|
}
|
|
});
|
|
},
|
|
showDialogTags() {
|
|
this.dialogTags.visible = true;
|
|
},
|
|
showDialogRenameTag(idx, tag) {
|
|
this.showDialog({
|
|
title: "Rename Tag",
|
|
content: `Change the name for tag "#${tag.name}"`,
|
|
fields: [{
|
|
name: "newName",
|
|
label: "New tag name",
|
|
value: tag.name,
|
|
}],
|
|
mainText: "OK",
|
|
secondText: "Cancel",
|
|
secondClick: () => {
|
|
this.dialog.visible = false;
|
|
this.dialogTags.visible = true;
|
|
},
|
|
mainClick: (data) => {
|
|
// Send data
|
|
tag.name = data.newName;
|
|
|
|
this.dialog.loading = true;
|
|
fetch("/api/tag", {
|
|
method: "PUT",
|
|
body: JSON.stringify(tag),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) throw response;
|
|
return response.json();
|
|
})
|
|
.then(() => {
|
|
this.dialog.loading = false;
|
|
this.dialog.visible = false;
|
|
this.dialogTags.visible = true;
|
|
this.tags.splice(idx, 1, tag);
|
|
})
|
|
.catch(err => {
|
|
this.dialog.loading = false;
|
|
this.dialogTags.visible = false;
|
|
this.dialogTags.editMode = false;
|
|
err.text().then(msg => {
|
|
this.showErrorDialog(msg, err.status);
|
|
})
|
|
});
|
|
},
|
|
});
|
|
},
|
|
},
|
|
mounted() {
|
|
// Prepare history state watcher
|
|
var stateWatcher = (e) => {
|
|
var state = e.state || {},
|
|
activePage = state.activePage || "page-home",
|
|
search = state.search || "",
|
|
page = state.page || 1;
|
|
|
|
if (activePage !== "page-home") return;
|
|
|
|
this.page = page;
|
|
this.search = search;
|
|
this.loadData(false);
|
|
}
|
|
|
|
window.addEventListener('popstate', stateWatcher);
|
|
this.$once('hook:beforeDestroy', function() {
|
|
window.removeEventListener('popstate', stateWatcher);
|
|
})
|
|
|
|
// Set initial parameter
|
|
var url = new Url,
|
|
initialPage = url.hash.replace(/^([^?]+).*$/, "$1");
|
|
|
|
if (initialPage === "home") {
|
|
var urlHash = url.hash.replace(initialPage, ""),
|
|
search = urlHash.replace(/^.*(\?|&)search=([^?&]*).*$/, "$2"),
|
|
page = urlHash.replace(/^.*(\?|&)page=(\d+).*$/, "$2");
|
|
|
|
this.search = decodeURIComponent(search) || "";
|
|
this.page = parseInt(page) || 1;
|
|
}
|
|
|
|
this.loadData(false, true);
|
|
}
|
|
} |