PluginProbe
Vimeography: Vimeo Video Gallery WordPress Plugin / 2.2
Vimeography: Vimeo Video Gallery WordPress Plugin v2.2
2.4.9 2.4.8 trunk 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 0.6 0.6.1 0.6.2 0.6.3 0.6.4 0.6.5 0.6.6 0.6.7 0.6.8 0.6.8.1 0.6.9 0.6.9.1 0.6.9.2 0.7 0.8 All 103 releases
vimeography / lib / admin / app / src / providers / Gallery.tsx

Gallery.tsx in Vimeography: Vimeo Video Gallery WordPress Plugin 2.2, at lib/admin/app/src/providers/Gallery.tsx

422 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import * as React from "react";
2 import GalleryContext from "../context/Gallery";
3 import { useQuery } from "react-query";
4 import produce from "immer";
5 import { Helmet } from "react-helmet";
6 import { useDebouncedCallback } from "use-debounce";
7
8 import { Theme, ThemeSetting, ThemeSettingType } from "~/providers/Themes";
9 import ThemesContext from "../context/Themes";
10
11 type GalleryProviderProps = React.PropsWithChildren<{ id?: string }>;
12
13 export type GalleryState = {
14 id?: number;
15 cache_timeout?: number;
16 date_created?: string;
17 featured_video?: string;
18 gallery_width?: string;
19 resource_uri?: string;
20 source_url?: string;
21 theme_name?: string;
22 title?: string;
23 video_limit?: number;
24 appearanceRules?: GalleryAppearanceRule[];
25 allow_downloads?: boolean;
26 sort?: SortType;
27 direction?: SortDirection;
28 enable_search?: boolean;
29 enable_tags?: boolean;
30 enable_playlist?: boolean;
31 per_page?: number;
32 };
33
34 type SortDirection = "asc" | "desc";
35 type SortType =
36 | "date"
37 | "likes"
38 | "comments"
39 | "plays"
40 | "alphabetical"
41 | "duration"
42 | "default";
43 interface GalleryResponse {
44 id: number;
45 cache_timeout: number;
46 date_created: string;
47 featured_video: string;
48 gallery_width: string;
49 resource_uri: string;
50 source_url: string;
51 theme_name: string;
52 title: string;
53 video_limit: number;
54 allow_downloads?: boolean;
55 sort?: SortType;
56 direction?: SortDirection;
57 enable_search?: boolean;
58 enable_tags?: boolean;
59 enable_playlist?: boolean;
60 per_page?: number;
61 }
62
63 export type GalleryAppearanceRule = {
64 id: string;
65 css: string;
66 };
67
68 type Action =
69 | { type: `HYDRATE`; payload: GalleryResponse }
70 | { type: `EDIT_GALLERY_STATE`; payload: GalleryState }
71 | { type: `RESET_GALLERY_APPEARANCE` }
72 | { type: `UPDATE_GALLERY_APPEARANCE`; payload: ThemeSetting };
73
74 const initialState = {
75 appearanceRules: [],
76 };
77
78 const convertToDashedAttribute = (attr: string) => {
79 let shimmedAttribute;
80
81 // backwards-compat for old gallery theme settings
82 // removes grid prefix from deprecated theme attributes
83 // for browser compatibility
84 if (attr === `gridColumnGap`) {
85 shimmedAttribute = `columnGap`;
86 } else if (attr === `gridRowGap`) {
87 shimmedAttribute = `rowGap`;
88 } else {
89 shimmedAttribute = attr;
90 }
91
92 return shimmedAttribute.replace(/[A-Z]/g, (a) => "-" + a.toLowerCase());
93 };
94
95 const computeValue = (
96 type: ThemeSettingType,
97 transform: string = "",
98 value: string
99 ) => {
100 const suffix = type === "numeric" || type === "slider" ? "px" : "";
101
102 if (transform !== "") {
103 return `${transform.replace(/{{value}}/, value + suffix)}`;
104 } else {
105 return `${value}${suffix}`;
106 }
107 };
108
109 const reducer = (state: GalleryState, action: Action) => {
110 switch (action.type) {
111 case "HYDRATE":
112 case `EDIT_GALLERY_STATE`: {
113 return {
114 ...state,
115 ...action.payload,
116 };
117 }
118
119 case `RESET_GALLERY_APPEARANCE`: {
120 return produce(state, (next) => {
121 next.appearanceRules = [];
122 });
123 }
124
125 case `UPDATE_GALLERY_APPEARANCE`: {
126 let rules: GalleryAppearanceRule[] = [];
127
128 const buildCSS = (
129 target: string,
130 attribute: string,
131 computedValue: string,
132 label: string
133 ) =>
134 `/* ${label} */
135 ${
136 namespace ? "#vimeography-gallery-" + state.id : ""
137 }${target} { ${attribute}: ${computedValue}${
138 important ? " !important;" : ";"
139 } }
140 `;
141
142 const {
143 id,
144 label,
145 type,
146 value,
147 properties,
148 namespace,
149 important,
150 expressions = [],
151 } = action.payload;
152
153 properties.map((property, index) => {
154 const computedValue = computeValue(type, property.transform, value);
155 const attribute = convertToDashedAttribute(property.attribute);
156
157 rules.push({
158 id: `${id}-property-${index}`,
159 css: buildCSS(property.target, attribute, computedValue, label),
160 });
161 });
162
163 expressions.map((expression, index) => {
164 const attribute = convertToDashedAttribute(expression.attribute);
165
166 let calculatedValue;
167
168 switch (expression.operator) {
169 case "+":
170 calculatedValue = Math.ceil(
171 parseInt(value) + eval(expression.value)
172 );
173 break;
174 case "-":
175 calculatedValue = Math.ceil(
176 parseInt(value) - eval(expression.value)
177 );
178 break;
179 case "/":
180 calculatedValue = Math.ceil(
181 parseInt(value) / eval(expression.value)
182 );
183 break;
184 case "*":
185 calculatedValue = Math.ceil(
186 parseInt(value) * eval(expression.value)
187 );
188 break;
189 }
190
191 const computedValue = computeValue(
192 type,
193 expression.transform,
194 calculatedValue.toString()
195 );
196
197 rules.push({
198 id: `${id}-expression-${index}`,
199 css: buildCSS(expression.target, attribute, computedValue, label),
200 });
201 });
202
203 return produce(state, (next) => {
204 rules.map((rule) => {
205 const index = next.appearanceRules.findIndex(
206 (el) => el.id === rule.id
207 );
208
209 if (index === -1) {
210 next.appearanceRules.push(rule);
211 } else {
212 next.appearanceRules[index] = rule;
213 }
214 });
215 });
216 }
217
218 default:
219 // console.log(`unknown action type: ${action.type}`);
220 return state;
221 }
222 };
223
224 const GalleryProvider = (props: GalleryProviderProps) => {
225 const themesCtx = React.useContext(ThemesContext);
226 const [state, dispatch] = React.useReducer(reducer, initialState);
227
228 const { isLoading, error, data } = useQuery(
229 [`galleries`, props.id],
230 () => {
231 return fetch(
232 window.vimeographyApiSettings.root +
233 `vimeography/v1/galleries/${props.id}`
234 ).then((res) => {
235 return res.json();
236 });
237 },
238 { staleTime: Infinity }
239 );
240
241 React.useEffect(() => {
242 if (!data) return;
243 dispatch({ payload: data, type: "HYDRATE" });
244 }, [data]);
245
246 // Detect any theme CSS customizations on load and
247 // send them to the themes context to hydrate
248 // the appearance controls with the correct values
249 React.useEffect(() => {
250 if (!data) return;
251 if (!themesCtx.data) return;
252
253 const activeTheme: Theme = themesCtx.data.find(
254 (theme: Theme) => theme.name === data.theme_name
255 );
256
257 if (!activeTheme) return;
258
259 const customStyle = document.getElementById(
260 `vimeography-gallery-${props.id}-custom-css`
261 );
262
263 if (!customStyle) return;
264 console.log(`custom style found!`);
265 console.log(customStyle);
266 console.log(`rules:`);
267 console.dir(customStyle.sheet.cssRules);
268 // since it is possible that the element it hidden (as in a modal)
269 // try to get the defined value based on the loaded stylesheet
270 activeTheme.settings.map((setting) => {
271 setting.properties.map((prop) => {
272 // build the expected selector
273
274 // accounts for incorrect single-colon pseudo selectors in any theme settings
275 // https://regex101.com/r/zzlv1J/1
276 const modifiedTarget = prop.target.replace(
277 /([^:])(:)([^:])/g,
278 "$1::$3"
279 );
280
281 const selectorToMatch = setting.namespace
282 ? `#vimeography-gallery-${props.id}${modifiedTarget}`
283 : modifiedTarget;
284
285 console.log(
286 `attempting to match theme setting in custom stylesheet rules: ${selectorToMatch}`
287 );
288
289 // search the stylesheet for the selector
290 for (let rule of customStyle.sheet.cssRules) {
291 // need to find a match for the target defined in our theme settings
292
293 if (rule.selectorText === selectorToMatch) {
294 // get the value for the current prop attribute
295
296 console.log(`we have a match`);
297
298 let value = rule.style.getPropertyValue(
299 convertToDashedAttribute(prop.attribute)
300 );
301
302 // if no value, no customization found for this particular setting
303 if (!value) continue;
304
305 // console.log(`value found for ${selectorToMatch}!`);
306 // console.log(value);
307
308 // check if it needs to be un-transformed to extract the value
309 if (prop.transform) {
310 // console.log(
311 // `extracting value from transform ${prop.transform}`
312 // );
313
314 const searchTerm = `{{value}}`;
315
316 // find the index of `{{value}}` in the transform
317 const tokenIndex = prop.transform.indexOf(searchTerm);
318
319 // find the characters which appear after the searchTerm in the transform
320 const terminator = prop.transform.substr(
321 tokenIndex + searchTerm.length
322 );
323
324 // get the index of the terminating characters within the existing customized css value
325 const terminatorIndex = value.indexOf(terminator);
326
327 // pull the value out of the string
328 const extractedValue = value.substring(
329 tokenIndex,
330 terminatorIndex
331 );
332
333 // console.log(
334 // `extracted ${extractedValue} as the value based on the transform`
335 // );
336
337 value = extractedValue;
338 }
339
340 // reformat the incoming `action.payload.value` to the expected format for the ui control based on the settings type `setting.type`
341
342 let computedValue;
343
344 // console.log(setting);
345
346 switch (setting.type) {
347 case "colorpicker": {
348 // { r: 51, g: 51, b: 51, a: 1 } react-color may already convert this for us, test!
349 computedValue = value;
350 break;
351 }
352
353 case "numeric":
354 case "slider": {
355 // must be a number
356 const strippedValue = value.replace("px", "");
357 const convertedValue = Math.ceil(strippedValue);
358
359 computedValue = convertedValue;
360 break;
361 }
362
363 default: {
364 computedValue = value;
365 break;
366 }
367 }
368
369 console.log(`updating value for ${setting.id} to ${computedValue}`);
370
371 // update the default `activeTheme`s `setting.value` for `setting.id` to the determined configured `value`
372 themesCtx.dispatch({
373 type: `THEME.SETTING.UPDATE_DEFAULT_VALUE`,
374 payload: {
375 themeName: activeTheme.name,
376 settingId: setting.id,
377 value: computedValue,
378 },
379 });
380
381 dispatch({
382 type: `UPDATE_GALLERY_APPEARANCE`,
383 payload: { ...setting, value: computedValue },
384 });
385 }
386 }
387 });
388 });
389
390 // Remove the style that came from the server since we'll be
391 // generating a new one in the client anyway
392 customStyle.remove();
393 }, [data, themesCtx]);
394
395 return (
396 <GalleryContext.Provider
397 value={{
398 isLoading,
399 error,
400 data,
401 state,
402 dispatch,
403 }}
404 >
405 <Helmet>
406 <script type="text/javascript">{`!function(e,t,n){function a(){var e=t.getElementsByTagName("script")[0],n=t.createElement("script");n.type="text/javascript",n.async=!0,n.src="https://beacon-v2.helpscout.net",e.parentNode.insertBefore(n,e)}if(e.Beacon=n=function(t,n,a){e.Beacon.readyQueue.push({method:t,options:n,data:a})},n.readyQueue=[],"complete"===t.readyState)return a();e.attachEvent?e.attachEvent("onload",a):e.addEventListener("load",a,!1)}(window,document,window.Beacon||function(){});`}</script>
407 <script type="text/javascript">{`window.Beacon('init', 'f7cb9d02-1ec7-4320-ad10-fcbc85e4d544')`}</script>
408 <style
409 type="text/css"
410 id={`vimeography-gallery-${props.id}-custom-css-preview`}
411 >{`
412 ${state.appearanceRules.map((rule) => rule.css).join("\r\n\r\n")}
413 `}</style>
414 </Helmet>
415
416 {props.children}
417 </GalleryContext.Provider>
418 );
419 };
420
421 export default GalleryProvider;
422