Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Page Select Component #2582

Merged
merged 5 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/assets/javascripts/alchemy/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,4 @@
//= require alchemy/alchemy.list_filter
//= require alchemy/alchemy.uploader
//= require alchemy/alchemy.preview_window
//= require alchemy/page_select
//= require alchemy/node_select
46 changes: 0 additions & 46 deletions app/assets/javascripts/alchemy/page_select.js

This file was deleted.

42 changes: 42 additions & 0 deletions app/components/alchemy/admin/page_select.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module Alchemy
module Admin
class PageSelect < ViewComponent::Base
delegate :alchemy, to: :helpers

def initialize(page = nil, url: nil, allow_clear: false, placeholder: Alchemy.t(:search_page), query_params: nil)
@page = page
@url = url
@allow_clear = allow_clear
@placeholder = placeholder
@query_params = query_params
end

def call
content_tag("alchemy-page-select", content, attributes)
end

private

def attributes
options = {
placeholder: @placeholder,
url: @url || alchemy.api_pages_path
}

options = options.merge({"allow-clear": @allow_clear}) if @allow_clear
options = options.merge({"query-params": @query_params.to_json}) if @query_params

if @page
selection = {
id: @page.id,
name: @page.name,
url_path: @page.url_path
}
options = options.merge({selection: selection.to_json})
end

options
end
end
end
end
1 change: 1 addition & 0 deletions app/javascript/alchemy_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ $.fx.speeds._default = 400
import "alchemy_admin/components/char_counter"
import "alchemy_admin/components/datepicker"
import "alchemy_admin/components/overlay"
import "alchemy_admin/components/page_select"
import "alchemy_admin/components/spinner"
import "alchemy_admin/components/tinymce"
import "alchemy_admin/components/tooltip"
Expand Down
42 changes: 21 additions & 21 deletions app/javascript/alchemy_admin/components/alchemy_html_element.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { toCamelCase } from "alchemy_admin/utils/string_conversions"

export class AlchemyHTMLElement extends HTMLElement {
static properties = {}

Expand Down Expand Up @@ -25,16 +27,16 @@ export class AlchemyHTMLElement extends HTMLElement {
* @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Components#reference
*/
connectedCallback() {
// parse the properties object and register property variables
Object.keys(this.constructor.properties).forEach((propertyName) => {
// parse the properties object and register property with the default values
Object.keys(this.constructor.properties).forEach((name) => {
// if the options was given via the constructor, they should be prefer (e.g. new <WebComponentName>({title: "Foo"}))
if (this.options[propertyName]) {
this[propertyName] = this.options[propertyName]
} else {
this._updateProperty(propertyName, this.getAttribute(propertyName))
}
this[name] =
this.options[name] ?? this.constructor.properties[name].default
})

// then process the attributes
this.getAttributeNames().forEach((name) => this._updateFromAttribute(name))

// render the component
this._updateComponent()
this.connected()
Expand All @@ -54,8 +56,8 @@ export class AlchemyHTMLElement extends HTMLElement {
* triggered by the browser, if one of the observed attributes is changing
* @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Components#reference
*/
attributeChangedCallback(name, oldValue, newValue) {
this._updateProperty(name, newValue)
attributeChangedCallback(name) {
this._updateFromAttribute(name)
this._updateComponent()
}

Expand Down Expand Up @@ -96,23 +98,21 @@ export class AlchemyHTMLElement extends HTMLElement {
}

/**
* update the property value
* if the value is undefined the default value is used
* update the value from the given attribute
*
* @param {string} propertyName
* @param {string} value
* @param {string} name
* @private
*/
_updateProperty(propertyName, value) {
const property = this.constructor.properties[propertyName]
_updateFromAttribute(name) {
const attributeValue = this.getAttribute(name)
const propertyName = toCamelCase(name)
const isBooleanValue =
attributeValue.length === 0 || attributeValue === "true"

const value = isBooleanValue ? true : attributeValue

if (this[propertyName] !== value) {
this[propertyName] = value
if (
typeof property.default !== "undefined" &&
this[propertyName] === null
) {
this[propertyName] = property.default
}
this.changeComponent = true
}
}
Expand Down
120 changes: 120 additions & 0 deletions app/javascript/alchemy_admin/components/page_select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { AlchemyHTMLElement } from "./alchemy_html_element"

class PageSelect extends AlchemyHTMLElement {
static properties = {
allowClear: { default: false },
selection: { default: undefined },
placeholder: { default: "" },
queryParams: { default: "{}" },
url: { default: "" }
}

connected() {
sascha-karnatz marked this conversation as resolved.
Show resolved Hide resolved
this.input.classList.add("alchemy_selectbox")

const dispatchCustomEvent = (name, detail = {}) => {
this.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }))
}

$(this.input)
.select2(this.select2Config)
.on("select2-open", (event) => {
// add focus to the search input. Select2 is handling the focus on the first opening,
// but it does not work the second time. One process in select2 is "stealing" the focus
// if the command is not delayed. It is an intermediate solution until we are going to
// move away from Select2
setTimeout(() => {
document.querySelector("#select2-drop .select2-input").focus()
}, 100)
})
.on("change", (event) => {
if (event.added) {
dispatchCustomEvent("Alchemy.PageSelect.PageAdded", event.added)
} else {
dispatchCustomEvent("Alchemy.PageSelect.PageRemoved")
}
})
}

get input() {
return this.getElementsByTagName("input")[0]
}

get select2Config() {
return {
placeholder: this.placeholder,
allowClear: this.allowClear,
initSelection: (_$el, callback) => {
if (this.selection) {
callback(JSON.parse(this.selection))
}
},
ajax: this.ajaxConfig,
formatSelection: this._renderResult,
formatResult: this._renderListEntry
}
}

/**
* Ajax configuration for Select2
* @returns {object}
*/
get ajaxConfig() {
const data = (term, page) => {
return {
q: { name_cont: term, ...JSON.parse(this.queryParams) },
page: page
}
}

const results = (data) => {
const meta = data.meta
return {
results: data.pages,
more: meta.page * meta.per_page < meta.total_count
}
}

return {
url: this.url,
datatype: "json",
quietMillis: 300,
data,
results
}
}

/**
* result which is visible if a page was selected
* @param {object} page
* @returns {string}
* @private
*/
_renderResult(page) {
return page.text || page.name
}

/**
* html template for each list entry
* @param {object} page
* @returns {string}
* @private
*/
_renderListEntry(page) {
return `
<div class="page-select--page">
<div class="page-select--top">
<i class="icon far fa-file fa-lg"></i>
<span class="page-select--page-name">${page.name}</span>
<span class="page-select--page-urlname">${page.url_path}</span>
</div>
<div class="page-select--bottom">
<span class="page-select--site-name">${page.site.name}</span>
<span class="page-select--language-code">${page.language.name}</span>
</div>
</div>
`
}
}

customElements.define("alchemy-page-select", PageSelect)
10 changes: 10 additions & 0 deletions app/javascript/alchemy_admin/utils/string_conversions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* convert dashes and underscore strings into camelCase strings
* @param {string} str
* @returns {string}
*/
export function toCamelCase(str) {
return str
.split(/-|_/)
.reduce((a, b) => a + b.charAt(0).toUpperCase() + b.slice(1))
}
39 changes: 19 additions & 20 deletions app/views/alchemy/admin/nodes/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= alchemy_form_for([:admin, node]) do |f| %>
<%= alchemy_form_for([:admin, node], id: "node_form") do |f| %>
<% if node.new_record? && node.root? %>
<%= f.input :menu_type,
collection: Alchemy::Language.current.available_menu_names.map { |n| [I18n.t(n, scope: [:alchemy, :menu_names]), n] },
Expand All @@ -13,7 +13,9 @@
value: node.page && node.read_attribute(:name).blank? ? nil : node.name,
placeholder: node.page ? node.page.name : nil
} %>
<%= f.input :page_id, label: Alchemy::Page.model_name.human, input_html: { class: 'alchemy_selectbox' } %>
<%= render Alchemy::Admin::PageSelect.new(node.page, allow_clear: true) do %>
<%= f.input :page_id, label: Alchemy::Page.model_name.human %>
<% end %>
<%= f.input :url, input_html: { disabled: node.page }, hint: Alchemy.t(:node_url_hint) %>
<%= f.input :title %>
<%= f.input :nofollow %>
Expand All @@ -26,23 +28,20 @@
<% end %>

<script>
$('#node_page_id').alchemyPageSelect({
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
placeholder: "<%= Alchemy.t(:search_page) %>",
url: "<%= alchemy.api_pages_path %>",
<% if node.page %>
initialSelection: {
id: <%= node.page_id %>,
text: "<%= node.page.name %>",
url_path: "<%= node.page.url_path %>"
}
<% end %>
}).on('change', function(e) {
if (e.val === '') {
sascha-karnatz marked this conversation as resolved.
Show resolved Hide resolved
$('#node_name').removeAttr('placeholder')
$('#node_url').val('').prop('disabled', false)
} else {
$('#node_name').attr('placeholder', e.added.name)
$('#node_url').val(e.added.url_path).prop('disabled', true)
}
const nodeName = document.getElementById("node_name")
const nodeUrl = document.getElementById("node_url")
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
const form = document.getElementById("node_form")

form.addEventListener("Alchemy.PageSelect.PageAdded", (event) => {
const page = event.detail
nodeName.setAttribute("placeholder", page.name)
nodeUrl.value = page.url_path
nodeUrl.setAttribute("disabled", "disabled")
})

form.addEventListener("Alchemy.PageSelect.PageRemoved", (event) => {
nodeName.removeAttribute("placeholder")
nodeUrl.value = ""
nodeUrl.removeAttribute("disabled")
})
</script>
19 changes: 3 additions & 16 deletions app/views/alchemy/admin/pages/_form.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<%= alchemy_form_for [:admin, @page], class: 'edit_page' do |f| %>
<% unless @page.language_root? || @page.layoutpage %>
<%= f.input :parent_id, required: true, input_html: { class: 'alchemy_selectbox' } %>
<%= render Alchemy::Admin::PageSelect.new(@page.parent) do %>
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved
<%= f.input :parent_id, required: true %>
<% end %>
<% end %>

<div class="input check_boxes">
Expand Down Expand Up @@ -52,18 +54,3 @@

<%= f.submit Alchemy.t(:save) %>
<% end %>

<script>
$('#page_parent_id').alchemyPageSelect({
placeholder: "<%= Alchemy.t(:search_page) %>",
url: "<%= alchemy.api_pages_path %>",
allowClear: false,
<% if @page.parent %>
initialSelection: {
id: <%= @page.parent.id %>,
text: "<%= @page.parent.name %>",
url_path: "<%= @page.parent.url_path %>"
}
<% end %>
})
</script>
Loading
Loading