/** * Internal dependencies */ import { settingIdToWidgetId } from '../../utils' const { wp } = window function parseWidgetId(widgetId) { const matches = widgetId.match(/^(.+)-(\d+)$/) if (matches) { return { idBase: matches[1], number: parseInt(matches[2], 10), } } // Likely an old single widget. return { idBase: widgetId } } function widgetIdToSettingId(widgetId) { const { idBase, number } = parseWidgetId(widgetId) if (number) { return `widget_${idBase}[${number}]` } return `widget_${idBase}` } /** * This is a custom debounce function to call different callbacks depending on * whether it's the _leading_ call or not. * * @param {Function} leading The callback that gets called first. * @param {Function} callback The callback that gets called after the first time. * @param {number} timeout The debounced time in milliseconds. * @return {Function} The debounced function. */ function debounce(leading, callback, timeout) { let isLeading = false let timerID function debounced(...args) { const result = (isLeading ? callback : leading).apply(this, args) isLeading = true clearTimeout(timerID) timerID = setTimeout(() => { isLeading = false }, timeout) return result } debounced.cancel = () => { isLeading = false clearTimeout(timerID) } return debounced } export default class SidebarAdapter { constructor(setting, api) { this.setting = setting this.api = wp.customize this.locked = false this.widgetsCache = new WeakMap() this.subscribers = new Set() this.history = [ this._getWidgetIds().map((widgetId) => this.getWidget(widgetId)), ] this.historyIndex = 0 this.historySubscribers = new Set() // Debounce the input for 1 second. this._debounceSetHistory = debounce( this._pushHistory, this._replaceHistory, 1000 ) this.setting.bind(this._handleSettingChange.bind(this)) this.api.bind('change', this._handleAllSettingsChange.bind(this)) this.undo = this.undo.bind(this) this.redo = this.redo.bind(this) this.save = this.save.bind(this) } subscribe(callback) { this.subscribers.add(callback) return () => { this.subscribers.delete(callback) } } getWidgets() { return this.history[this.historyIndex] } _emit(...args) { for (const callback of this.subscribers) { callback(...args) } } _getWidgetIds() { return this.setting.get() } _pushHistory() { this.history = [ ...this.history.slice(0, this.historyIndex + 1), this._getWidgetIds().map((widgetId) => this.getWidget(widgetId)), ] this.historyIndex += 1 this.historySubscribers.forEach((listener) => listener()) } _replaceHistory() { this.history[this.historyIndex] = this._getWidgetIds().map((widgetId) => this.getWidget(widgetId) ) } _handleSettingChange() { if (this.locked) { return } const prevWidgets = this.getWidgets() this._pushHistory() this._emit(prevWidgets, this.getWidgets()) } _handleAllSettingsChange(setting) { if (this.locked) { return } if (!setting.id.startsWith('widget_')) { return } const widgetId = settingIdToWidgetId(setting.id) if (!this.setting.get().includes(widgetId)) { return } const prevWidgets = this.getWidgets() this._pushHistory() this._emit(prevWidgets, this.getWidgets()) } _createWidget(widget) { const widgetModel = wp.customize.Widgets.availableWidgets.findWhere({ id_base: widget.idBase, }) let number = widget.number if (widgetModel.get('is_multi') && !number) { widgetModel.set('multi_number', widgetModel.get('multi_number') + 1) number = widgetModel.get('multi_number') } const settingId = number ? `widget_${widget.idBase}[${number}]` : `widget_${widget.idBase}` const settingArgs = { transport: wp.customize.Widgets.data.selectiveRefreshableWidgets[ widgetModel.get('id_base') ] ? 'postMessage' : 'refresh', previewer: this.setting.previewer, } const setting = this.api.create(settingId, settingId, '', settingArgs) setting.set(widget.instance) const widgetId = settingIdToWidgetId(settingId) return widgetId } _removeWidget(widget) { const settingId = widgetIdToSettingId(widget.id) const setting = this.api(settingId) if (setting) { const instance = setting.get() this.widgetsCache.delete(instance) } this.api.remove(settingId) } _updateWidget(widget) { const prevWidget = this.getWidget(widget.id) // Bail out update if nothing changed. if (prevWidget === widget) { return widget.id } // Update existing setting if only the widget's instance changed. if ( prevWidget.idBase && widget.idBase && prevWidget.idBase === widget.idBase ) { const settingId = widgetIdToSettingId(widget.id) this.api(settingId).set(widget.instance) return widget.id } // Otherwise delete and re-create. this._removeWidget(widget) return this._createWidget(widget) } getWidget(widgetId) { if (!widgetId) { return null } const { idBase, number } = parseWidgetId(widgetId) const settingId = widgetIdToSettingId(widgetId) const setting = this.api(settingId) if (!setting) { return null } const instance = setting.get() if (this.widgetsCache.has(instance)) { return this.widgetsCache.get(instance) } const widget = { id: widgetId, idBase, number, instance, } this.widgetsCache.set(instance, widget) return widget } _updateWidgets(nextWidgets) { this.locked = true const addedWidgetIds = [] const nextWidgetIds = nextWidgets.map((nextWidget) => { if (nextWidget.id && this.getWidget(nextWidget.id)) { addedWidgetIds.push(null) return this._updateWidget(nextWidget) } const widgetId = this._createWidget(nextWidget) addedWidgetIds.push(widgetId) return widgetId }) const deletedWidgets = this.getWidgets().filter( (widget) => !nextWidgetIds.includes(widget.id) ) deletedWidgets.forEach((widget) => this._removeWidget(widget)) this.setting.set(nextWidgetIds) this.locked = false return addedWidgetIds } setWidgets(nextWidgets) { const addedWidgetIds = this._updateWidgets(nextWidgets) this._debounceSetHistory() return addedWidgetIds } /** * Undo/Redo related features */ hasUndo() { return this.historyIndex > 0 } hasRedo() { return this.historyIndex < this.history.length - 1 } _seek(historyIndex) { const currentWidgets = this.getWidgets() this.historyIndex = historyIndex const widgets = this.history[this.historyIndex] this._updateWidgets(widgets) this._emit(currentWidgets, this.getWidgets()) this.historySubscribers.forEach((listener) => listener()) this._debounceSetHistory.cancel() } undo() { if (!this.hasUndo()) { return } this._seek(this.historyIndex - 1) } redo() { if (!this.hasRedo()) { return } this._seek(this.historyIndex + 1) } subscribeHistory(listener) { this.historySubscribers.add(listener) return () => { this.historySubscribers.delete(listener) } } save() { this.api.previewer.save() } }