full customize tags

This commit is contained in:
nitikornbunya 2025-01-23 15:08:09 +07:00
parent 8fb1d73def
commit 1d4eb697a0
6 changed files with 382 additions and 76 deletions

View File

@ -284,18 +284,23 @@ export default {
const originalTag = this.$store.get('page/tags')
// RegEx # (/tag), @ (), ! ()
const regex = /(?:^|\s|>)([#@!][\w\u0E00-\u0E7F-]+)/g;
const regex = /(?:^|\s|>)([#@!][\w\u0E00-\u0E7F-]+(?:\.[^\s<>#@!]+)*)/g;
// matches #, @, !
const matches = [...content.matchAll(regex)].map(match => match[1].substring(1));
const matches = [...content.matchAll(regex)].map(match => {
if (match[1].startsWith('#')) {
return match[1].substring(1);
}
return match[1];
});
console.log('จาก detect: ', matches); // result = ['', '', '', '', '123', '14', '', '', '']
console.log('tags เริ่มต้น: ', originalTag); // result = ['tag', __ob__: Observer]
// console.log(' detect: ', matches); // result = ['', '', '', '', '123', '14', '', '', '']
// console.log('tags : ', originalTag); // result = ['tag', __ob__: Observer]
// originalTag matches
const combinedTags = Array.from(new Set([...originalTag, ...matches]));
console.log('Combined Tags (No Duplicates):', combinedTags);
// console.log('Combined Tags (No Duplicates):', combinedTags);
// Store ()
this.$store.set('page/tags', matches);

View File

@ -64,6 +64,9 @@ export default {
},
async fetchTags(queryText) {
try {
// Delay search by 300ms
await new Promise(resolve => setTimeout(resolve, 500));
const response = await this.$apollo.query({
query: gql`
query {
@ -82,10 +85,12 @@ export default {
return [];
}
// array string object
// array string object tag @ !
const allTags = response.data.pages.tags
.filter((tag, index, self) =>
index === self.findIndex(t => t.id === tag.id)
index === self.findIndex(t => t.id === tag.id) &&
!tag.tag.startsWith('@') &&
!tag.tag.startsWith('!')
)
.map(t => ({
text: '#' + t.tag, // filter insert
@ -98,11 +103,130 @@ export default {
label: tag.title // dropdown
}));
// autocomplete list @ !
if (!allTags.some(tag => tag.text === `#${queryText}`) &&
!queryText.startsWith('@') &&
!queryText.startsWith('!')) {
allTags.push({
id: `#${queryText}`,
text: `#${queryText}`,
label: `${queryText}`
});
}
return allTags;
} catch (err) {
console.error('Error fetching tags:', err)
return []
}
},
async fetchPeople(queryText) {
try {
// Delay search by 300ms
await new Promise(resolve => setTimeout(resolve, 500));
const response = await this.$apollo.query({
query: gql`
query {
pages {
tags {
id
tag
title
}
}
}
`
})
if (!response.data.pages || !response.data.pages.tags) {
return [];
}
// tag @
const allPeople = response.data.pages.tags
.filter((tag, index, self) =>
index === self.findIndex(t => t.id === tag.id) &&
tag.tag.startsWith('@')
)
.map(t => ({
id: t.tag,
text: t.tag,
// tag label
label: t.title || t.tag
}))
// filter
.filter(person => {
const searchText = queryText.toLowerCase();
return person.text.toLowerCase().includes(searchText) ||
person.label.toLowerCase().includes(searchText);
});
// list
if (queryText && !allPeople.some(person => person.text === `@${queryText}`)) {
allPeople.push({
id: `@${queryText}`,
text: `@${queryText}`,
label: `@${queryText}`
});
}
return allPeople;
} catch (err) {
console.error('Error fetching people:', err)
return []
}
},
async fetchPlace(queryText) {
try {
// Delay search by 300ms
await new Promise(resolve => setTimeout(resolve, 500));
const response = await this.$apollo.query({
query: gql`
query {
pages {
tags {
id
tag
title
}
}
}
`
})
if (!response.data.pages || !response.data.pages.tags) {
return [];
}
// tag !
const allPlaces = response.data.pages.tags
.filter((tag, index, self) =>
index === self.findIndex(t => t.id === tag.id) &&
tag.tag.startsWith('!')
)
.map(t => ({
id: t.tag,
text: t.tag,
label: t.title || t.tag.substring(1) // ! title
}))
.filter(place => place.text.toLowerCase().includes(queryText.toLowerCase()));
// list
if (!allPlaces.some(place => place.text === `!${queryText}`)) {
allPlaces.push({
id: `!${queryText}`,
text: `!${queryText}`,
label: `!${queryText}`
});
}
return allPlaces;
} catch (err) {
console.error('Error fetching places:', err)
return []
}
}
},
async mounted() {
@ -112,14 +236,6 @@ export default {
language: this.locale,
placeholder: 'Type the page content here',
disableNativeSpellChecker: false,
htmlSupport: {
allow: [
{
name: 'span',
classes: ['hashtag-text']
}
]
},
mention: {
feeds: [
{
@ -127,11 +243,23 @@ export default {
feed: async (queryText) => {
return await this.fetchTags(queryText)
},
dropdownLimit: 10,
minimumCharacters: 2,
itemRenderer: item => {
const div = document.createElement('div');
div.classList.add('custom-item', 'hashtag-item');
// <span> hashtag symbol text
const span = document.createElement('span');
span.classList.add('hashtag-text');
span.textContent = `#${item.label}`;
div.appendChild(span);
return div;
},
// handler # dropdown
dropdownOnEmpty: true, // dropdown
defaultItem: (queryText) => {
console.log(queryText)
return {
id: queryText,
text: `#${queryText}`,
@ -148,28 +276,49 @@ export default {
},
{
marker: '@',
feed: (queryText) => {
// expert-directory
return [
'@นิธิกร.บุญยกุลเจริญ',
'@จาบอน.จันทร์สุข',
'@ทิม.พิธา'
].filter(user => user.toLowerCase().includes(queryText.toLowerCase()))
feed: async (queryText) => {
return await this.fetchPeople(queryText)
},
minimumCharacters: 2
dropdownLimit: 10,
minimumCharacters: 2,
// itemRenderer
itemRenderer: item => {
const div = document.createElement('div');
div.classList.add('custom-item', 'mention-item');
//
const nameSpan = document.createElement('span');
nameSpan.classList.add('mention-text');
nameSpan.textContent = item.label;
div.appendChild(nameSpan);
return div;
},
// defaultItem
defaultItem: (queryText) => ({
id: queryText,
text: `@${queryText}`,
label: `@${queryText}`
})
},
{
marker: '!',
feed: (queryText) => {
//
return [
'!bangkok',
'!london',
'!newyork'
].filter(location => location.toLowerCase().includes(queryText.toLowerCase()))
feed: async (queryText) => {
return await this.fetchPlace(queryText)
},
minimumCharacters: 2
dropdownLimit: 10,
minimumCharacters: 2,
itemRenderer: item => {
const div = document.createElement('div');
div.classList.add('custom-item', 'place-item');
div.textContent = item.label;
return div;
},
defaultItem: (queryText) => ({
id: queryText,
text: `!${queryText}`,
label: queryText
})
}
]
},
@ -197,8 +346,6 @@ export default {
(match, space, tag) => `${space}<span class="hashtag-text">#${tag}</span>`
);
console.log(content);
this.$store.set('editor/content', beautify(content, {
indent_size: 2,
end_with_newline: true
@ -363,7 +510,7 @@ $editor-height-mobile: calc(100vh - 56px - 16px);
padding: 8px 12px;
.hashtag-symbol {
color: #1976d2; //
color: #ce5c19; //
font-weight: bold;
margin-right: 2px;
}
@ -416,4 +563,48 @@ $editor-height-mobile: calc(100vh - 56px - 16px);
}
}
}
.mention-item {
display: flex;
align-items: center;
padding: 8px 12px;
.mention-text {
color: #333;
&:hover {
color: #1976d2;
}
}
}
.theme--dark {
.mention-item {
.mention-text {
color: #fff;
&:hover {
color: #64b5f6;
}
}
}
}
// mention
.ck-content {
.mention {
background: unset;
color: #1976d2 !important;
font-weight: 500;
&:hover {
text-decoration: underline;
cursor: pointer;
}
}
}
.theme--dark .ck-content {
.mention {
color: #64b5f6 !important;
}
}
</style>

View File

@ -106,25 +106,25 @@
v-list-item-title.px-3.caption.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-1`') {{tocSubItem.title}}
//- v-divider(inset, v-if='tocIdx < toc.length - 1')
v-card.page-tags-card.mb-5(v-if='tags.length > 0')
.pa-5
.overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') {{$t('common:page.tags')}}
v-chip.mr-1.mb-1(
label
:color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`'
v-for='(tag, idx) in tags'
:href='`/t/` + tag.tag'
:key='`tag-` + tag.tag'
)
v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', left, small) mdi-tag
span(:class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`') {{tag.title}}
v-chip.mr-1.mb-1(
label
:color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`'
:href='`/t/` + tags.map(t => t.tag).join(`/`)'
:aria-label='$t(`common:page.tagsMatching`)'
)
v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', size='20') mdi-tag-multiple
//- v-card.page-tags-card.mb-5(v-if='tags.length > 0')
//- .pa-5
//- .overline.teal--text.pb-2(:class='$vuetify.theme.dark ? `text--lighten-3` : ``') {{$t('common:page.tags')}}
//- v-chip.mr-1.mb-1(
//- label
//- :color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`'
//- v-for='(tag, idx) in tags'
//- :href='`/t/` + tag.tag'
//- :key='`tag-` + tag.tag'
//- )
//- v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', left, small) mdi-tag
//- span(:class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`') {{tag.title}}
//- v-chip.mr-1.mb-1(
//- label
//- :color='$vuetify.theme.dark ? `teal darken-1` : `teal lighten-5`'
//- :href='`/t/` + tags.map(t => t.tag).join(`/`)'
//- :aria-label='$t(`common:page.tagsMatching`)'
//- )
//- v-icon(:color='$vuetify.theme.dark ? `teal lighten-3` : `teal`', size='20') mdi-tag-multiple
v-card.page-comments-card.mb-5(v-if='commentsEnabled && commentsPerms.read')
.pa-5
@ -183,35 +183,35 @@
.page-author-card-date.caption.grey--text.text--darken-1 {{ updatedAt | moment('calendar') }}
//- PPle customize
v-card.page-author-card.mb-5
v-card.page-author-card.mb-5(v-if='tags.filter(t => !t.tag.startsWith("@") && !t.tag.startsWith("!")).length > 0')
.pa-5
.overline.indigo--text.d-flex(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
span คคล
span Tags
v-spacer
ul.tags-list
li.tag-item(v-for='(tag, idx) in tags' :key='`tag-` + tag.tag')
a(:href='`/คน/` + tag.tag', target="_blank", :class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`')
li.tag-item(v-for='tag in tags.filter(t => !t.tag.startsWith("@") && !t.tag.startsWith("!"))' :key='`tag-` + tag.tag')
a(:href='`/t/` + tag.tag', target="_blank", :class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`')
| {{ tag.title }}
v-card.page-author-card.mb-5
v-card.page-author-card.mb-5(v-if='tags.filter(t => t.tag.startsWith("@")).length > 0')
.pa-5
.overline.indigo--text.d-flex(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
span คน
v-spacer
ul.tags-list
li.tag-item(v-for='tag in tags.filter(t => t.tag.startsWith("@"))' :key='`tag-` + tag.tag')
a(:href='`/คน/` + tag.tag.substring(1)', target="_blank", :class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`')
| {{ tag.title.substring(1) }}
v-card.page-author-card.mb-5(v-if='tags.filter(t => t.tag.startsWith("!")).length > 0')
.pa-5
.overline.indigo--text.d-flex(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
span สถานท
v-spacer
ul.tags-list
li.tag-item(v-for='(tag, idx) in tags' :key='`tag-` + tag.tag')
a(:href='`/สถานที่/` + tag.tag', target="_blank", :class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`')
| {{ tag.title }}
v-card.page-author-card.mb-5
.pa-5
.overline.indigo--text.d-flex(:class='$vuetify.theme.dark ? `text--lighten-3` : ``')
span เหตการณ
v-spacer
ul.tags-list
li.tag-item(v-for='(tag, idx) in tags' :key='`tag-` + tag.tag')
a(:href='`/เหตุการณ์/` + tag.tag', target="_blank", :class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`')
| {{ tag.title }}
li.tag-item(v-for='tag in tags.filter(t => t.tag.startsWith("!"))' :key='`tag-` + tag.tag')
a(:href='`/สถานที่/` + tag.tag.substring(1)', target="_blank", :class='$vuetify.theme.dark ? `teal--text text--lighten-5` : `teal--text text--darken-2`')
| {{ tag.title.substring(1) }}
//- v-card.mb-5
//- .pa-5

View File

@ -685,7 +685,7 @@
display:inline-block;
vertical-align:top;
padding-top:0;
&:first-child {
width: 100%;
}
@ -1152,7 +1152,7 @@
}
.mention {
background-color: rgba(153, 0, 48, .1);
// background-color: rgba(153, 0, 48, .1);
color: #990030;
@at-root .theme--dark & {

View File

@ -5,7 +5,7 @@
if (node.nodeType === Node.TEXT_NODE) {
// Replace special tags in text content only
const updatedText = node.textContent.replace(
/(?:^|\s)([#@!])([\w\u0E00-\u0E7F-]+)/g,
/(?:^|\s)([#@!])([\w\u0E00-\u0E7F-]+(?:[-\.]\w+)*)/g,
(match, symbol, keyword) => {
const trimmedMatch = match.trim(); // Remove leading space for correct replacement
if (symbol === '#') {

View File

@ -0,0 +1,110 @@
<script>
document.addEventListener("DOMContentLoaded", () => {
// Function to process text nodes
const processTextNodes = (node) => {
if (node.nodeType === Node.TEXT_NODE) {
// Replace special tags in text content only
const updatedText = node.textContent.replace(
/(#([\w\u0E00-\u0E7F-]+)|!([\w\u0E00-\u0E7F-]+)|&([\w\u0E00-\u0E7F-]+))/g,
(match) => {
if (match.startsWith('#')) {
const keyword = match.substring(1);
return `<a target="_blank" href="/คน/${keyword}" class="tag tag-hashtag">${match}</a>`;
} else if (match.startsWith('!')) {
const keyword = match.substring(1);
return `<a target="_blank" href="/สถานที่/${keyword}" class="tag tag-mention">${match}</a>`;
} else if (match.startsWith('&')) {
const keyword = match.substring(1);
return `<a target="_blank" href="/เหตุการณ์/${keyword}" class="tag tag-location">${match}</a>`;
}
return match;
}
);
// If there's an update, replace the node with a new element
if (updatedText !== node.textContent) {
const wrapper = document.createElement('span');
wrapper.innerHTML = updatedText;
node.replaceWith(wrapper);
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
// Recursively process child nodes
node.childNodes.forEach(processTextNodes);
}
};
// Function to initialize processing
const initializeObserver = () => {
const contentContainer = document.querySelector('.contents');
// Retry if content container is not found
if (!contentContainer) {
setTimeout(initializeObserver, 100); // Retry after 100ms
return;
}
console.log('Content container found:', contentContainer);
// Initial processing of the content
processTextNodes(contentContainer);
// Setup a MutationObserver to monitor changes in content
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' || mutation.type === 'characterData') {
mutation.target.childNodes.forEach(processTextNodes);
}
}
});
// Observe changes in the container
observer.observe(contentContainer, { childList: true, subtree: true });
};
// Start the initialization process
initializeObserver();
});
</script>
<style type="text/css">
.v-application .primary {
background-color: #ef6c00 !important;
border-color: #ef6c00 !important
}
.v-application p,
li {
font-family: Sarabun !important;
}
.v-application .blue.darken-3 {
background-color: #ef6c00 !important;
border-color: #ef6c00 !important;
}
.v-application .blue.darken-2 {
background-color: #ffa346 !important;
border-color: #ffa346 !important;
}
h1 {
color: #ef6c00 !important;
}
.v-main .contents h1:after {
background: linear-gradient(90deg, #f57c00, rgba(25, 118, 210, 0));
}
.__bar-is-vertical {
background: #FF9800 !important;
}
b,
strong {
font-weight: 600 !important;
}
.page-header-section {
display: none !important;
}
</style>